Elemento simplifies working with Elemental2. In a nutshell Elemento brings the following features to the table:
- Type safe builders, event handlers and CSS selectors
- Helper methods to manipulate the DOM tree
- Execute asynchronous tasks in parallel, in sequence or as long as a certain condition is met.
- Simple, non-invasive, slash-based router with support for parameters and asynchronous loaders (
/a/:b/c
) - Thin logging wrapper around
console.log
using categories, log levels, and a predefined log format. - Ready to be used with GWT and J2CL
- Minimal dependencies
- Elemental2 (
elemental2-core
,elemental2-dom
andelemental2-webstorage
) - GWT (
org.gwtproject.event:gwt-event
andorg.gwtproject.safehtml:gwt-safehtml
)
- Elemental2 (
TOC
- Get Started
- Builder API
- Event Handlers
- Typesafe CSS Selectors
- Custom Elements
- Goodies
- SVG & MathML
- Flow
- Router
- Logger
- Samples
- Contributing
- Get Help
Elemento is available in Maven Central. The easiest way is to import its BOM
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.elemento</groupId>
<artifactId>elemento-bom</artifactId>
<version>1.6.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
and add a dependency to either
<dependency>
<groupId>org.jboss.elemento</groupId>
<artifactId>elemento-core</artifactId>
<type>gwt-lib</type>
</dependency>
or
<dependency>
<groupId>org.jboss.elemento</groupId>
<artifactId>elemento-core</artifactId>
</dependency>
depending on your stack. If you're using GWT, inherit from org.jboss.elemento.Core
:
<module>
<inherits name="org.jboss.elemento.Core"/>
</module>
When working with GWT Elemental it is often awkward and cumbersome to create a hierarchy of elements. Even simple structures like
<section class="main">
<input class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li>
<div class="view">
<input class="toggle" type="checkbox" checked>
<label>Taste Elemento</label>
<button class="destroy"></button>
</div>
<input class="edit">
</li>
</ul>
</section>
lead to a vast amount of Document.createElement()
and chained Node.appendChild()
calls. With Elemento creating the above structure is as easy as
import static org.jboss.elemento.Elements.*;
import static org.jboss.elemento.InputType.checkbox;
import static org.jboss.elemento.InputType.text;
HTMLElement section = section().css("main")
.add(input(checkbox).id("toggle-all").css("toggle-all"))
.add(label()
.apply(l -> l.htmlFor = "toggle-all")
.textContent("Mark all as complete"))
.add(ul().css("todo-list")
.add(li()
.add(div().css("view")
.add(input(checkbox)
.css("toggle")
.checked(true))
.add(label().textContent("Taste Elemento"))
.add(button().css("destroy")))
.add(input(text).css("edit"))))
.element();
The class Elements
provides convenience methods to create the most common elements. It uses a fluent API to create and append elements on the fly. Take a look at the API documentation for more details.
When creating large hierarchies of elements you often need to assign an element somewhere in the tree. Use an inline assignment together with element()
to create and assign the element in one go:
import static org.jboss.elemento.Elements.*;
final HTMLElement count;
final HTMLElement footer = footer()
.add(count = span().css("todo-count").element())
.element();
Elemento provides methods to easily register event handlers. There are constants for most of the known event types. You can either add event handlers when building the element hierarchy:
import static org.jboss.elemento.Elements.*;
import static org.jboss.elemento.EventType.*;
import static org.jboss.elemento.InputType.checkbox;
import static org.jboss.elemento.InputType.text;
HTMLLIElement listItem = li()
.add(div().css("view")
.add(input(checkbox)
.css("toggle")
.on(change, event -> toggle()))
.add(label()
.textContent("Taste Elemento")
.on(dblclick, event -> edit()))
.add(button()
.css("destroy")
.on(click, event -> destroy())))
.add(input(text)
.css("edit")
.on(keydown, this::keyDown)
.on(blur, event -> blur()))
.element();
or register them later using EventType.bind()
:
import org.gwtproject.event.shared.HandlerRegistration;
import static elemental2.dom.DomGlobal.alert;
import static org.jboss.elemento.EventType.bind;
import static org.jboss.elemento.EventType.click;
HandlerRegistration handler = bind(listItem, click, event -> alert("Clicked"));
The latter approach returns org.gwtproject.event.shared.HandlerRegistration
which you can use to remove the handler again.
In order to make it easier to work with keyboard events, Elemento provides an enum with the most common keyboard codes:
import elemental2.dom.KeyboardEvent;
import static org.jboss.elemento.Key.Escape;
import static org.jboss.elemento.Key.Enter;
void keyDown(KeyboardEvent event) {
if (Escape.match(event)) {
...
} else if (Enter.match(event)) {
...
}
}
Elemento provides a typesafe selector API. It can be used to express simple CSS selector like .class
or #id
up to complex selectors like
#main [data-list-item|=foo] a[href^="https://"] > .fas.fa-check, .external[hidden]
This selector can be created with
import org.jboss.elemento.By;
import static org.jboss.elemento.By.AttributeOperator.CONTAINS_TOKEN;
import static org.jboss.elemento.By.AttributeOperator.STARTS_WITH;
By selector = By.group(
By.id("main")
.desc(By.data("listItem", CONTAINS_TOKEN, "foo")
.desc(By.element("a").and(By.attribute("href", STARTS_WITH, "https://"))
.child(By.classname(new String[]{"fas", "fa-check"})))),
By.classname("external").and(By.attribute("hidden"))
);
The selector can be used to find single or all HTML elements:
import org.jboss.elemento.By;
import static org.jboss.elemento.By.AttributeOperator.STARTS_WITH;
import static org.jboss.elemento.Elements.a;
import static org.jboss.elemento.Elements.body;
By selector = By.element("a").and(By.attribute("href", STARTS_WITH, "https://"));
for (HTMLElement element : body().findAll(selector)) {
a(element).css("external");
}
Elemento makes it easy to create custom elements. As for Elemento custom elements are a composite of HTML elements and / or other custom elements. They're ordinary classes which can hold state or register event handlers. The only requirement is to implement IsElement<E extends HTMLElement>
and return a root element:
import static org.jboss.elemento.Elements.*;
class TodoItemElement implements IsElement<HTMLElement> {
private final HTMLElement root;
private final HTMLInputElement toggle;
private final HTMLElement label;
private final HTMLInputElement summary;
TodoItemElement(TodoItem item) {
this.root = li().data("item", item.id)
.add(div().css("view")
.add(toggle = input(checkbox).css("toggle")
.checked(item.completed)
.element())
.add(label = label().textContent(item.text).element())
.add(destroy = button().css("destroy").element()))
.add(summary = input(text).css("edit").element())
.element();
this.root.classList.toggle("completed", item.completed);
}
@Override
public HTMLElement element() {
return root;
}
// event handlers omitted
}
The builder API has support for IsElement<E extends HTMLElement>
which makes it easy to use custom elements when building the element hierarchy:
import static org.jboss.elemento.Elements.ul;
TodoItemRepository repository = ...;
TodoItemElement[] itemElements = repository.items().stream()
.map(TodoItemElement::new)
.toArray();
ul().addAll(itemElements).element();
Besides the builder API, Elemento comes with a bunch of static helper methods that roughly fall into these categories:
- Get notified when an element is attached to and detached from the DOM tree.
- Iterate over elements.
- Methods to manipulate the DOM tree (add, insert and remove elements).
- Methods to manipulate an element.
- Methods to generate safe IDs.
See the API documentation of Elements for more details.
Implement Attachable
to get notified when an element is attached to and detached from the DOM tree. The attachable interface provides a static method to easily register the callbacks to attach(MutationRecord)
and detach(MutationRecord)
:
import elemental2.dom.MutationRecord;
import org.jboss.elemento.Attachable;
import org.jboss.elemento.IsElement;
import static elemental2.dom.DomGlobal.console;
import static org.jboss.elemento.Elements.li;
class TodoItemElement implements IsElement<HTMLElement>, Attachable {
private final HTMLElement root;
TodoItemElement(TodoItem item) {
this.root = li().element();
Attachable.register(root, this);
}
@Override
public HTMLElement element() {
return root;
}
@Override
public void attach(MutationRecord mutationRecord) {
console.log("Todo item has been attached");
}
@Override
public void detach(MutationRecord mutationRecord) {
console.log("Todo item has been detached");
}
}
Elemento uses the MutationObserver
API to detect changes in the DOM tree and passes an MutationRecord
instance to the attach(MutationRecord)
and detach(MutationRecord)
methods. This instance contains additional information about the DOM manipulation.
Important
The attach(MutationRecord)
and detach(MutationRecord)
methods are only called once after the attachable implementation has been registered with Attachable.register(HTMLElement, Attachable)
. If you hold a reference to the element, and the element is attached to the DOM again, the callbacks are not called again, unless the element has been registered again. This is done for reasons of optimisation and performance.
Tip
If you want to be notified every time your custom element is attached to the DOM it is recommended to add the call to Attachable.register(HTMLElement, Attachable)
in the constructor like in the example above and recreate the custom element.
Elemento provides several methods to iterate over node lists, child elements or elements returned by a selector. There are methods which return Iterator
, Iterable
and Stream
.
-
Elements.iterator(JsArrayLike<E> nodes)
Returns an iterator over the given array-like. The iterator does not support theIterator.remove()
operation. -
Elements.iterator(Node parent)
Returns an iterator over the children ofparent
. The iterator supports theIterator.remove()
operation, which removes the current node from its parent.
-
Elements.elements(JsArrayLike<E> nodes)
Returns an iterable for the nodes in the given array-like. -
Elements.children(elemental2.dom.Node parent)
Returns an iterable for the child nodes ofparent
.
-
Elements.stream(JsArrayLike<E> nodes)
Returns a stream for the nodes in the given array-like. -
Elements.stream(elemental2.dom.Node parent)
Returns a stream for the child nodes ofparent
.
-
Elements.lazyAppend(Element parent, Element child)
Appendschild
toparent
. Ifparent
already containschild
, this method does nothing. -
Elements.insertAfter(Element newElement, Element after)
InsertsnewElement
intoafter
's parent afterafter
. -
Elements.lazyInsertAfter(Element newElement, Element after)
InsertsnewElement
intoafter
's parent afterafter
. Ifafter
's parent already containsnewElement
, this method does nothing. -
Elements.insertBefore(Element newElement, Element before)
InsertsnewElement
intobefore
's parent beforebefore
. -
Elements.lazyInsertBefore(Element new Element, Element before)
InsertsnewElement
intobefore
's parent beforebefore
. Ifbefore
's parent already containsnewElement
, this method does nothing. -
Elements.insertFirst(Element parent, Element newElement)
InsertsnewElement
as first element intoparent
. -
Elements.removeChildrenFrom(Element element)
Removes all child elements fromelement
-
Elements.failSafeRemove(Node parent, Element child)
Removeschild
fromparent
. -
Elements.failSafeRemoveFromParent(Element element)
Removeselement
from its parent.
All methods are null-safe, check parent/child relationships, and are overloaded to accept an instance of IsElement<Element>
instead of Element
. See the API documentation of Elements for more details.
Elemento comes with basic support for SVG and MathML.
To create SVG elements, add the following dependency to your POM:
<dependency>
<groupId>org.jboss.elemento</groupId>
<artifactId>elemento-svg</artifactId>
<version>1.6.0</version>
</dependency>
In your GWT module inherit from org.jboss.elemento.SVG
:
<module>
<inherits name="org.jboss.elemento.SVG"/>
</module>
Finally, use the static methods in org.jboss.elemento.svg.SVG
to create SVG elements.
To create MathML elements, add the following dependency to your POM:
<dependency>
<groupId>org.jboss.elemento</groupId>
<artifactId>elemento-mathml</artifactId>
<version>1.6.0</version>
</dependency>
In your GWT module inherit from org.jboss.elemento.MathML
:
<module>
<inherits name="org.jboss.elemento.MathML"/>
</module>
Finally, use the static methods in org.jboss.elemento.mathml.MathML
to create MathML elements.
The class Flow provides methods to execute a list of asynchronous tasks in parallel or sequentially, or to execute a single task repeatedly as long as certain conditions are met.
See the API documentation of org.jboss.elemento.flow
for more details.
// datetime format is "2022-03-31T11:03:39.348365+02:00"
Task<FlowContext> currentTime = context -> fetch("https://worldtimeapi.org/api/timezone/Europe/Berlin")
.then(Response::json)
.then(json -> Promise.resolve(Js.<JsPropertyMap<String>>cast(json).get("datetime").substring(11, 23)))
.then(context::resolve);
double ms = 500 + new Random().nextInt(2000);
Task<FlowContext> delay = context -> new Promise<>((res, __) -> setTimeout(___ -> res.onInvoke(context), ms));
// execute the two tasks in parallel
Flow.parallel(new FlowContext(), List.of(currentTime, delay))
.subscribe(context -> console.log("Current time: " + context.pop("n/a")));
// datetime format is "2022-03-31T11:03:39.348365+02:00"
Task<FlowContext> currentTime = context -> fetch("https://worldtimeapi.org/api/timezone/Europe/Berlin")
.then(Response::json)
.then(json -> Promise.resolve(Js.<JsPropertyMap<String>>cast(json).get("datetime").substring(11, 23)))
.then(context::resolve);
double ms = 500 + new Random().nextInt(2_000);
Task<FlowContext> delay = context -> new Promise<>((res, __) -> setTimeout(___ -> res.onInvoke(context), ms));
// execute the two tasks in sequence and cancel after 1_000 ms
Flow.parallel(new FlowContext(), List.of(currentTime, delay))
.timeout(1_000)
.subscribe(context -> console.log("Current time: " + context.pop("n/a")));
Task<FlowContext> currentTime = context -> fetch("https://worldtimeapi.org/api/timezone/Europe/Berlin")
.then(Response::json)
.then(json -> Promise.resolve(Js.<JsPropertyMap<String>>cast(json).get("datetime").substring(11, 23)))
.then(context::resolve);
// fetch the current time until the milliseconds end with "0" and cancel after 5 iterations
Flow.repeat(new FlowContext(),currentTime)
.while_(context -> !context.pop("").endsWith("0"))
.iterations(5)
.subscribe(context -> console.log("Current time: " + context.pop("n/a")));
Elemento offers a very basic router. The router is minimal invasive and built around a few simple concepts:
-
Place
Data class that represents a place in an application. A place is identified by a route, and can have an optional title and a custom root element. Routes can have parameters like in/contacts/:contactId
. Finally, a place can also have a loader assigned to it. -
Places
Builder to set up places. Supports nested places, combining places and allows to assign loaders to places. -
LoadData<T>
Functional interface to asynchronously load data before a page is added to the DOM. The function gets the place and parameters as input, returns a promise of the data to be loaded and is defined asPromise<T> load(Place place, Parameter parameter)
. -
Page
Simple interface that represents a collection of HTML elements. Implementations should be cheap to create and need to implement a single method:Iterable<HTMLElement> elements(Place, Parameter, LoadedData)
. -
PlaceManager
Main class of the router module. performs the following tasks, among others:- keeps track of registered places
- handles navigation events
- parses route parameters
- calls loader functions
- updates the DOM and browser history
The place manager can be customized using builder like methods and has a start()
method to show the initial page.
Creating a Places
instance can be time-consuming (especially in large applications). This can be automated by using annotations, where an annotation processor takes over the tedious work of creating the Places
instance. You can use the following annotations for this purpose:
-
@Route
Annotation to mark aPage
implementation as place. The annotation requires a route and can have an optional title, root selector and reference to a named loader. If the annotated page has exactly one public, static method that returnsLoadData<?>
and accepts no parameters, this is used as loader for the place. If a loader is specified using theloader
attribute, there has to be a matching class or static method annotated with@Loader
. -
@Loader
Annotation to mark a class or public static method as a named loader. If used on a class, the class has to implementLoadData<T>
. If used on a static method, the method has to returnLoadData<?>
and must not accept any parameters.
You can also mix and match an own Places
instance with the generated one (see below).
Here's an example showing most of the concepts in action:
@Route("/time/:area/:location")
public class TimePage implements Page {
public static LoadData<String> loadTime() {
return (place, parameter) -> {
String area = parameter.get("area");
String location = parameter.get("location");
String url = "https://worldtimeapi.org/api/timezone/" + area + "/" + location;
return fetch(url)
.then(Response::json)
.then(json -> {
JsPropertyMap<String> map = Js.cast(json);
return Promise.resolve(map.get("datetime"));
});
};
}
@Override
public Iterable<HTMLElement> elements(Place place, Parameter parameter, LoadedData data) {
String area = parameter.get("area");
String location = parameter.get("location");
String currentTime = data.get();
return asList(
h(1, "Current time").element(),
p()
.add("It's ")
.add(span().textContent(currentTime))
.add(" in " + area + "/" + location)
.element());
}
}
@Route("/", "Home")
public class HomePage implements Page {
@Override
public Iterable<HTMLElement> elements(Place place, Parameter parameter, LoaderData data) {
return asList(
h(1, "Welcome").element(),
p()
.add("What time is it in ")
.add(a("/time/Europe/Berlin").textNode("Berlin"))
.add("?")
.element());
}
}
public class Application {
public void entryPoint() {
body().add(div().id("main"));
Places allPlaces = places()
.add(new AnnotatedPlaces()) // generated places instance
.add(place("/foo"), FooPage::new)
.add(place("/bar"), BarPage::new)
.children("/level1", places()
.add(place("/"), Level1Page::new) // index page
.add(places() // siblings
.add(place("/foo"), Foo1Page::new)
.add(place("/bar"), Bar1Page::new))
.children("/level2", places()
.add(place("/foo"), Foo2Page::new)
.add(place("/bar"), Bar2Page::new)));
PlaceManager placeManager = new PlaceManager()
.root(By.id("main"))
.register(allPlaces);
placeManager.start();
}
}
See the API documentation of PlaceManager for more details.
Elemento contains a small wrapper around console.log
that uses categories, log levels, and a predefined log format.
The different log methods delegate to the corresponding methods in console
:
Logger.error(String, Object... params)
→console.error()
Logger.warn(String, Object... params)
→console.warn()
Logger.info(String, Object... params)
→console.info()
Logger.debug(String, Object... params)
→console.debug()
To get a logger use Logger.getLogger(String category)
.
package org.acme;
public class Foo {
private static final Logger logger = Logger.getLogger(Foo.class.getName());
}
You can use an arbitrary string as category. By using a hierarchical category, you can override subcategories. String substitutions are supported, and you can pass a variable list of parameters to the log methods.
The log level is set globally for all categories using Logger.setLevel(Level level)
. You can override the level for one category using Logger.setLevel(String category, Level level)
. To reset a category, use Logger.resetLevel(String category)
. If the category contains .
, it is interpreted hierarchically. This means that if the category org.jboss
is overridden, this is also applied to all subcategories (unless overridden otherwise).
The log format is predefined as
HH:mm:ss.SSS <level> [<category>] <message>
and cannot be customized. If the category is a fully qualified class name, the package names are shortened. In any case the category is trimmed, and right aligned.
The logger module exports some methods with slightly adjusted signatures to JavaScript. You can use them for instance in the browser dev tools to control the global and category based log levels:
-
org.jboss.elemento.logger.Logger.setLevel(String level)
Sets the global log level -
org.jboss.elemento.logger.Logger.setLevel(String category, String level)
Overrides the log level for one category -
org.jboss.elemento.logger.Logger.resetLevel(String category)
Resets the log level for the category to the global log level
Please use the fully qualified name!
Elemento comes with different sample applications to showcase and test the various modules. They're available at https://hal.github.io/elemento/samples/. The source code is available the samples folder.
If you want to contribute to Elemento, please follow the steps in contribution.
If you need help feel free to contact us at Gitter, browse the API documentation or file an issue.