Errai 4 and jsinterop best practices

By Max Barkley, Software Engineer at Red Hat

About me

Software engineer at Red hat

  • Declarative web framework with JavaEE-like features for GWT
  • More on that later

lead developer of errai framework

  • Web-tooling for business analysts to author business rules and processes for IT systems

part of BRMS/JBPM web-tooling team

About my team

enterprise scale

  • 700 000 LOC targeting GWT*
  • Large, distributed team
  • Complex user interactions (text and graphical editors)

* Generated using David A. Wheeler's SLOCCount

how we use jsinterop

native ui development

  • All new UI work is done with Elemental 2 and HTML templating (from Errai)

runtime-pluggability

  • Developing support for plugins written in GWT or plain JS

What is errai?

a web framework built with gwt

  • Dependency Injection
  • Client-Side Templating
  • Two-Way Data-Binding
  • Client-Side JAX-RS Support
  • CDI Events over HTTP

What is errai?

declarative Good, Boilerplate bad

  • Annotation-Driven features express intent without boilerplate
  • Uses CDI APIs (dependency injection and CDI events)

compile-time safety

  • Leverages the Java type system whenever possible
  • Validate annotation use at compile-time

Outline

errai app basics

  • Dependency injection
  • Templating
  • Data-Binding

JS Interop Basics

  • @JsType(isNative=true)
  • @JsMethod
  • @JsProperty
  • @JsOverlay

jsinterop best practices

  • Abstract versus native methods
  • Interfaces versus Classes
  • The dangers of @JsType(isNative=true) method overrides

JS Interop Basics

Declarative JS Interoperability

  • JS interop is a set of annotations for defining interfaces between GWT-compiled and native JavaScript
  • Supports two use cases: consuming and publishing
  • In this talk we will focus on consuming

JS Interop Basics

@JsType

  • @JsType declares an API for a JavaScript object
  • When isNative=false (the default) the usage is for publishing
    • Preserves names of all public members so they can be called from external JavaScript
package org.errai.example;
@JsType
public class ExportedService {
    private String value;                      // Not exported

    public String getValue() { return value; } // Exported
}
// All good
var service = new org.errai.example.ExportedService();
service.getValue();

// This property is undefined -- the name has been obfuscated
service.value;

JS Interop Basics

@JsType(isNative=true)

  • A native @JsType is for consuming external JavaScript
  • Native @JsTypes have no code generated — they are compile-time bindings for external JavaScript objects
    • This means that methods cannot have implementations (with one exception)
package org.example;

@JsType(isNative=true)
public class Foo {
    public native void bar();
}
Foo foo = new Foo();
foo.bar();
var foo = new org.example.Foo();
foo.bar();

Java

JavaScript

JS Interop Basics

@JsMethod

  • We can use @JsMethod to change the names of compiled method invocation
@JsType(isNative=true)
public interface JSObject {
    @JsMethod(name="foo") String bar();
}
String value = jso.bar();
var value = jso.foo();

Java

JavaScript

JS Interop Basics

@JsProperty

  • We can use @JsProperty to change the names of compiled properties
@JsType(isNative=true)
public class InputElement {
    @JsProperty(name="value") public String inputValue;
}
String old = input.inputValue;
input.inputValue = "new";
var old = input.value;
input.value = "new";

Java

JavaScript

JS Interop Basics

@JsProperty

  • We can also use @JsProperty to compile method invocations to property access
    • Property name is inferred for getter/setter methods
@JsType(isNative=true)
public interface InputElement {
    @JsProperty String getValue();
    @JsProperty void setValue(String value);
}
String old = input.getValue();
input.setValue("new");
var old = input.value;
input.value = "new";

Java

JavaScript

JS Interop Basics

@JsOverlay

  • We can use @JsOverlay to add convenience methods on top of native @JsTypes
  • @JsOverlay methods have lots of restrictions
    • Can only access public members
    • Must be effectively final
@JsType(isNative=true)
public class NumberInputElement {
    public String value;

    @JsOverlay public final int getValue() {
        return Integer.parseInt(value);
    }
}

JS Interop Basics

more than one way to skin a cat...

  • Typically there are many ways we can choose to model JavaScript objects
  • Consider trying to model a property, method, and constructor in a JavaScript object, MouseEvent
    • altKey : boolean property
    • getModifierState() : method returning boolean
    • MouseEvent(type) : constructor taking a String for the event type

JS Interop Basics

interfaces

  • No need to specify @JsType name and namespace attributes
  • Can enforce read-only properties most easily
  • Extremely easy to mock
@JsType(isNative=true)
public interface MouseEvent {
    @JsProperty boolean getAltKey();
    boolean getModifierState();
}

Pros

  • Cannot model constructor
  • Extra boiler-plate when modelling properties

Cons

JS Interop Basics

Abstract classes

  • Can model anything an interface can
  • Can model the constructor
  • Can more easily model properties
@JsType(isNative=true, namespace=JsPackage.GLOBAL)
public abstract class MouseEvent {
    public MouseEvent(String typeArg) {}
    public final boolean altKey;
    public abstract boolean getModifierState();
}

Pros

  • Cannot easily call the modelled constructor
  • Modelling a read-only property with final make mocking harder
  • Have to make sure name and namespace are correct or else casting will fail at runtime

Cons

JS Interop Basics

classes

  • Can model anything an abstract class can
  • Can model the constructor (and actually call it!)
  • Can more easily model properties
@JsType(isNative=true, namespace=JsPackage.GLOBAL)
public class MouseEvent {
    public MouseEvent(String typeArg) {}
    public final boolean altKey;
    public native boolean getModifierState();
}

Pros

  • Same issues before with mocking altKey
    
  • Same issues as before with namespace
  • Methods must be native, so this is the hardest to mock

Cons

JS Interop Basics

Question: which approach is best?

  • For testability
  • For low boilerplate
  • For enforcing correct usage

errai basics

Declarative Dependency Injection

@ApplicationScoped
public class SingletonService {
    // ...
}
public class FooProducer {
    @Produces static Foo newFoo() {
        // ...
    }
}
  • Injection points are dependencies of a bean
public class ServiceConsumer {
    @Inject SingletonService service;
    // ...
}
public class FooConsumer {
    @Inject
    public FooConsumer(Foo foo) {
        // ...
    }
}
  • Beans are objects with managed deps and lifecycles

errai basics

Declarative Dependency Injection

  • Dependencies are resolved based on type and qualifiers
  • Qualifiers are annotations for beans and injection points
    • ​they remove ambiguity when injecting common supertypes
@Named("Visa")
public class Visa
    implements CreditCard {
    // ...
}
@Named("Amex")
public class Amex
    implements CreditCard {
    // ...
}
  • For example...
This is ambgiuous
@Inject CreditCard card;
And this is not
@Inject @Named("Visa") CreditCard card;

errai basics

Declarative Dependency Injection

  • Declaratively define app entrypoints
@EntryPoint
public class AppStartup {
    @PostConstruct
    public void start() {
        // Startup logic goes here...
    }
}
  • @EntryPoint is a scope (like @ApplicationScoped)
    • but it is eagerly instantiated​​
  • @PostConstruct is a life-cycle method, invoked after the bean is created

errai basics

inject elemental 2 types

@Inject HTMLInputElement input;
@Inject @Named("h1") HTMLHeadingElement title;
@Inject @Named("div") HTMLElement div;
  • Use the @Named qualifier for types with multiple tags

errai basics

injecting other js objects

  • Create producers for globally scoped JavaScript objects
@JsType(isNative = true)
public abstract class JQueryProducer {
    @Produces @JsProperty(namespace = JsPackage.GLOBAL)
    public static JQuery $;
}
@JsType(isNative = true)
public abstract class JQueryProducer {
    @Produces
    @JsProperty(name = "$", namespace = JsPackage.GLOBAL)
    public static native JQuery get();
}
@Inject JQuery $;

errai basics

injecting other js objects

  • Can use most CDI features when creating beans for native JavaScript objects
@JsType(isNative = true)
public abstract class FooProducer {
    @Produces
    public static native Foo getFoo(Bar bar);
}
  • @Produces methods can have dependencies via arguments
@JsType(isNative = true)
@Singleton
public class Foo {
    @Inject
    public Foo(Bar bar) {}
}
  • The container supports constructor injection for JavaScript objects

errai basics

injecting via interface

@JsType(isNative=true)
public interface MouseEvent {
    @JsProperty boolean getAltKey();
    boolean getModifierState();
}
@JsType(isNative=true,
        name="MouseEvent",
        namespace=JsPackage.GLOBAL)
public class ConstructableMouseEvent implements MouseEvent {
    public ConstructableMouseEvent(String type) {}
    @JsProperty @Override public native boolean getAltKey();
    @Override public native boolean getModifierState();

    @Produces @JsOverlay @Named("click")
    public static MouseEvent click() {
        return new ConstructableMouseEvent("click");
    }
}
@Inject
@Named("click")
MouseEvent event;

errai basics

Client-side Templating

  • Create UI components declaratively
  • A component has two parts:
    • Template (HTML, CSS/LESS)
    • @Templated bean (a Java class)

errai basics

Client-side Templating

  • A template consists of:
    • An HTML file (mandatory)
    • A CSS or LESS stylesheet (optional)

errai basics

Client-side Templating

  • The HTML file can be a fragment or a full page
<!-- Template as fragment -->
<form class="form">
    <input class="username" type="text">
    <input class="password" type="password">
</form>
<!DOCTYPE html>
<html>
<head></head>
<body>
<!-- Any fragment in the body can be used -->
    <form class="form">
        <input class="username" type="text">
        <input class="password" type="password">
    </form>
</body>
</html>

errai basics

Client-side Templating

  • A @Templated bean is a Java class that defines behaviour
  • Is declared with the @Templated annotation
    • the paths are optional if convention is followed
    • IsElement allows access to the root element (optional)
@Templated(value = "HomeScreen.html", stylesheet = "HomeScreen.css")
public class HomeScreen implements IsElement {
    // ...
}
  • If the above compiles, then so does this
@Templated
public class HomeScreen implements IsElement {
    // ...
}

errai basics

Client-side Templating

  • Use fragment identifiers to replace elements in your template with fields in your @Templated bean
  • In your template, a fragment identifier is an id, class, or data-field attribute value.
<form id="userForm">
    <input data-field="username" type="text">
    <input class="password" type="password">
</form>

errai basics

Client-side Templating

  • In your @Templated bean use @DataField to map fields to fragment identifiers
// The "userForm" fragment in "UserForm.html" is the component root
@Templated("#userForm")
public class UserForm {

    // Maps to fragment "username"
    @Inject @DataField HTMLInputElement username;

    // Maps to fragment "password"
    @Inject @DataField("password") HTMLInputElement pass;
}
<form id="userForm">
    <input data-field="username" type="text">
    <input class="password" type="password">
</form>

errai basics

Client-side Templating

  • Prefer using Elemental 2 for @DataFields
  • Can also use:
    • Other @Templated beans
    • Custom Element Wrappers
    • Web Components
    • GWT Widgets

errai basics

Client-side Templating

  • You can easily add your own element definitions
@JsType(isNative = true,
        name = "HTMLDivElement",
        namespace = JsPackage.GLOBAL)
@Element("div")
public class Div extends HTMLElement {
    // ...
}
  • @Element makes this type injectable
    • The container creates instances of Div by calling document.createElement("div") and casting
<div></div>

errai basics

Client-side Templating

  • Create special configurations of elements from Elemental 2
@JsType(isNative = true,
        name = "HTMLInputElement",
        namespace = JsPackage.GLOBAL)
@Element("input")
@Property(name = "type",
          value = "text")
public class TextInput
    extends HTMLInputElement {
}
  • @Property makes the container set the "type" attribute to the value "text"
<input type="text">
@JsType(isNative = true,
        name = "HTMLInputElement",
        namespace = JsPackage.GLOBAL)
@Element("input")
@Property(name = "type",
          value = "password")
public class PasswordInput
    extends HTMLInputElement {
}
<input type="password">

errai basics

Client-side Templating

  • Wrapping a web component works just like wrapping an element
@Element("paper-input")
@JsType(isNative = true,
        name = "HTMLElement",
        namespace = JsPackage.GLOBAL)
public class PaperInput extends HTMLElement {
    // ...
}
  • This is a wrapper for the Polymer paper-input element
  • It uses @Element because you can create web components with document.createElement
  • After adding Polymer to your host page, you can start injecting PaperInputs

errai basics

Client-side Templating

@JsType(isNative = true,
        name = "HTMLElement",
        namespace = JsPackage.GLOBAL)
public class PaperInput extends HTMLElement {
    // ...
}
  • @Element is optional
  • You can still use this in a template but then you can't inject it without writing a producer
@Templated
public class Form {
    @DataField
    private PaperInput = (PaperInput) DomGlobal
                                        .document
                                        .createElement("paper-input");
}

errai basics

Client-side Templating

  • @EventHandler declares handlers for GWT Widget events and native DOM events
  • Uses fragment identifiers to declare which UI element the handler is for

errai basics

Client-side Templating

@Templated
public class UserForm {
    // ...

    @Inject @DataField HTMLButtonElement submit; // Optional

    @EventHandler("submit")
    public void onClick(@ForEvent("click") MouseEvent event) {
        // ...
    }
}
  • This declares a listener for a native browser event
    • Listens for the "click" event
    • Listens on the "submit" element in the template
    • Doesn't require there to be a @DataField for "submit"

errai basics

Declarative Data-Binding

  • Data-binding synchronizes state between a data-model and UI elements 
  • Data-binding in Errai is two-way
    • model changes propogate to the UI
    • UI changes propogate to the model

errai basics

Declarative Data-Binding

  • Any type annotated with @Bindable can be used as a data-binding model
  • Properties of a model must be primitives, Strings, or other @Bindable types
  • Properties are inferred based on getter/setter names
    • ​This example has the properties "name" and "address"
@Bindable
public class User {
    private String name;
    private Address address; // Address must be @Bindable
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public Address getAddress() { return address; }
    public void setAddress(Address address) { this.address = address; }
}

errai basics

Declarative Data-Binding

  • Use @Model in UI components to declare a field as a data-binding model
  • Use @Bound to declare UI fields as being data-bound to model properties
    • Binds to property with field name by default
    • Can bind to nested properties using simple notation
@Templated
public class NewUserForm {
    @Inject @Model User user;

    @Inject @Bound @DataField TextBox name;
    @Inject @Bound("address.street") @DataField HTMLInputElement street;
    @Inject @Bound("address.number") @DataField HTMLInputElement number;
}

errai basics

custom element Data-Binding

  • Implement/extend HasValue<T> for behaviour
  • This is not the HasValue<T> in gwt-user
  • This is an Errai interface used by data-binding
  • Now data-binding will use the getValue/setValue methods overrides
@JsType(isNative = true)
public interface HasValue<T> {
  T getValue();
  void setValue(T value);
}

errai basics

custom element Data-Binding

@Element("paper-input")
@JsType(isNative = true, name = "HTMLElement", namespace = JsPackage.GLOBAL)
public abstract class PaperInput extends HTMLElement
                                 implements HasValue<String> {
  @Override @JsProperty public abstract String getValue();
  @Override @JsProperty public abstract void setValue(String value);
}
  • Declare an element property for binding with @JsProperty
  • Data-binding will use the setValue/getValue methods
    
  • Because of the @JsProperty annotations, these are compiled to property access
String old = input.getValue();
input.setValue("foo");
var old = input.value;
input.value = "foo";

Java

JavaScript

errai basics

custom element Data-Binding

  • You can do a similar trick if you'd like data-binding to use a method on the object
String old = input.getValue();
input.setValue("foo");
var old = input.getter();
input.setter("foo");

Java

JavaScript

@Override @JsMethod(name = "getter") public abstract String getValue();
@Override @JsMethod(name = "setter") public abstract void setValue(String value);

errai basics

custom element Data-Binding

  • For more complex bindings, use @JsOverlay method
@Element("a")
@JsType(isNative = true, name="HTMLAnchorElement", namespace=JsPackage.GLOBAL)
public class BindableEmailAnchor extends HTMLAnchorElement
                                 implements HasValue<String> {

  @JsOverlay @Override
  public String getValue() {
    return textContent;
  }

  @JsOverlay @Override
  public void setValue(final String value) {
    textContent = value;
    href = "mailto:" + value;
  }
}

best practices

testable code

  • Most advice this section is how to use JS Interop in ways that can be tested outside the browser

avoid incompatible semantics

  • Because native @JsTypes have no code generated for them, method overrides have different semantics

best practices

avoid native methods

  • Native methods make it harder to test usage of JS APIs.
  • You can use GWT Mockito, but it's not fool proof

best practices

avoid native methods

  • If you need to use a @JsType constructor on a type with methods, then you can't avoid them
@JsType(isNative=true, name="MouseEvent", namespace=JsPackage.GLOBAL)
public class ConstructableMouseEvent implements MouseEvent {
    public ConstructableMouseEvent(String type) {}
    public native boolean getModifierState();
}
  • But you can mitigate this by having most API in an interface, and limiting constructor usage to factory methods
public class MouseEventFactory {
    public MouseEvent create(String type) {
        return new ConstructableMouseEvent(type);
    }
}

best practices

interface + Class Method

  • Easy to mock interface usage, especially when used with dependency injection

Pros

  • Some boilerplate: have to duplicate all methods

Cons

best practices

avoid final methods/fields

  • Final members are also harder to mock in tests (can use Power Mockito, but also not fool proof)
  @JsOverlay @Override
  public final String getValue() {
    return textContent;
  }
  • @JsOverlay methods need only be effectively final
  @JsOverlay @Override
  public String getValue() {
    return textContent;
  }
@JsType(isNative=true,
        namespace=JsPackage.GLOBAL)
public class MouseEvent {
  public final boolean altKey;
}
  • Use getters instead of final fields
@JsType(isNative=true,
        namespace=JsPackage.GLOBAL)
public abstract class MouseEvent {
  @JsProperty
  public abstract boolean getAltKey();
}

best practices

dependency injection

  • Always use dependency injection for JS objects so you can mock them out in unit tests
@Templated
public class Form {
    @Inject
    public Form(JQuery $) {
        // ...
    }
}
@Test
public void formWithJQuery() {
    Form form = new Form(new JQueryMock());
    // ...
}

best practices

dependency injection

  • When you need JavaScript constructors, use dependency injection to wire factory methods referencing "constructable" @JsType classes
public class EventProducer {
    @Produces @Named("click") public MouseEvent click() {
        return new ConstructableMouseEvent("click");
    }
}

best practices

dependency injection

  • If you have too many, or a dynamic inputs, still use factories and inject the factories
public class MouseEventFactory {
    public MouseEvent create(String type) {
        return new ConstructableMouseEvent(type);
    }
}
public class Consumer {
    @Inject MouseEventFactory eventFactory;
}

best practices

avoid overrides and deep hiearchies

  • There is no dynamic dispatch for native @JsTypes
@Element("a")
@JsType(isNative = true, name="HTMLAnchorElement", namespace=JsPackage.GLOBAL)
public class BindableEmailAnchor extends HTMLAnchorElement
                                 implements HasValue<String> {
  @JsOverlay @Override
  public String getValue() {
    return textContent;
  }
  // ...
}
BindableEmailAnchor anchor = (BindableEmailAnchor) document.createElement("a");
String value1 = anchor.getValue();
String value2 = ((HasValue<String>) anchor).getValue();
  • What does this compile to?

best practices

avoid overrides and deep hiearchies

BindableEmailAnchor anchor = (BindableEmailAnchor) document.createElement("a");
String value1 = anchor.getValue();
String value2 = ((HasValue<String>) anchor).getValue();
  • It's not literally compiled to this, but code that is effectively the same
var anchor = document.createElement("a");
var value1 = anchor.textContent;
var value2 = anchor.getValue();
  • The GWT compiler gives different JavaScript calls depending on the static information at the call-site

best practices

avoid overrides and deep hiearchies

  • The same thing can happen with overrides that add or remove @JsProperty or @JsMethod
@Element("paper-input")
@JsType(isNative = true, name = "HTMLElement", namespace = JsPackage.GLOBAL)
public abstract class PaperInput extends HTMLElement
                                 implements HasValue<String> {
  @Override @JsProperty public abstract String getValue();
  @Override @JsProperty public abstract void setValue(String value);
}

best practices

avoid overrides and deep hiearchies

  • As a rule of thumb, do not override methods in native @JsTypes!
public PaperInputBindingHandler implements CustomBindingHandler<String> {
    private PaperInput obj;
    
    public PaperInputBindingHandler(PaperInput obj) {
        this.obj = obj;
    }

    @Override public String getValue() {
        return obj.getValue();
    }

    @Override public void setValue(String value) {
        obj.setValue(value);
    }
}
  • To make custom bindings work, we have to generate code with static call-sites for method overrides

take away

errai basics

  • Dependency Injection
@Inject
@Templated
@DataField
  • Templating
@Model
@Bound
  • Data-Binding
@Bindable

JsInterop tips

  • Separate JavaScript APIs into interfaces and "constructable" classes to make mocking/unit-testings easier
  • Use dependency injection to minimise referencing classes with native methods
  • Avoid method overrides

Thank you

Some Links