Interacting with JavaScript
TeaVM runs in browser and can't be isolated from browser's environment. Moreover, if you use TeaVM, you probably use it to alter an HTML page or draw something in Canvas element. Of course, TeaVM can interact with built-in JavaScript APIs as well as with existing JavaScript libraries. TeaVM was designed as a modular compiler, so core knows nothing about interaction with JavaScript. You need a TeaVM extension for this purpose. Fortunately, TeaVM comes with a bundled-in extension called JSO. Also, there is DOM module, that has existing wrappers around popular browser APIs. This section shows how to use JSO to interact with an existing JavaScript code.
Note, that if you are familiar with GWT, you can find that JSO concepts are quite similar to approach taken by GWT.
The API, described above, only works for JavaScript backend and is not designed for WebAssembly.
Maven dependencies
To create your own wrappers, you should include the following
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso</artifactId>
<version>0.10.2</version>
</dependency>
To use existing wrapper, you may also include the following
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-apis</artifactId>
<version>0.10.2</version>
</dependency>
Running JavaScript code from Java
To execute JavaScript code, you should declare native method and mark it with @JSBody annotation.
@JSBody
has two parameters.
First, params
, specifies which names in JavaScript correspond to parameters in Java, by position.
The number of items of params
array must be equal to the number of parameters of the method.
Second, script
, is JavaScript code.
Example:
@JSBody(params = { "message" }, script = "console.log(message)")
public static native void log(String message);
Using modules from @JSBody
It's possible to import external modules to use in @JSBody
scripts. For this purpose use imports
parameter.
For example, we have a module named testModule.js
:
export function foo() {
console.log("foo called");
}
to access this function, you can write following declaration in TeaVM:
@JSBody(
script = "return testModule.foo();",
imports = @JSBodyImport(
alias = "testModule",
fromModule = "testModule.js"
)
)
private static native int callModule();
Calling Java from JavaScript
There are two ways to call Java code from JavaScript.
The preferred way is to use overlay types and functors to pass Java callbacks to JavaScript.
Read about overlay types below.
Another way is to call Java method directly from @JSBody
script.
To call Java method from JavaScript, use the following syntax:
return javaMethods.get('method-reference').invoke(parameters);
where method-reference
consists of fully qualified class name followed by method descriptor as described in JVM Specification. Parameters should be specified like parameters of function invocation.
If you call member method, the first parameter corresponds to invocation target.
See example below:
@JSBody(params = { "str", "count" }, script = ""
+ "return javaMethods.get('java.lang.String.substring(II)Ljava/lang/String;')"
+ ".invoke(str, 0, count);")
public static native void left(String str, int count);
Note that you can't pass non-constant string to javaMethods.get
method.
Overlay types
Often you are not satisfied exchanging primitives with JavaScript. JSO comes with concept of overlay types, similar to overlay types in GWT. Overlay types allow to talk with JavaScript in terms of objects. However, as JSO is built upon Java, overlay objects are almost regular Java objects, so you get all advantages of static type system, IDE support, javadoc, etc.
An overlay type is a class or an interface that meets the following conditions:
- It must extend or implement JSObject interface, directly or indirectly.
- It must not have member fields.
- Final member methods must not implement or override parent method.
- In case of abstract class, it should extend either another overlay abstract class or
java.lang.Object
. - If it's non-abstract class, it should be annotated with
@JSClass
.
By default, each abstract or native method of an overlay class is mapped to a corresponding method of JavaScript, i.e. when you call a Java method, the JavaScript method of the same name is actually called. Parameters are converted before invocation to JavaScript and return value is converted back to Java.
For example,
public interface Node extends JSObject {
void appendChild(Node newChild);
Node cloneNode(boolean deep);
//...
}
Wrapping properties
To access JavaScript properties from Java, you should declare getter and setter methods, both optional. Getters and setters must satisfy Java Beans naming conventions. You also must annotate getters and setters with the @JSProperty annotation. By default, these methods will access the JavaScript property with the corresponding name, but you can define another property name in @JSProperty.
For example,
public interface HTMLElement extends Element {
@JSProperty
String getTitle();
@JSProperty()
void setTitle(String title);
//...
}
Extension methods
You can embed your custom logic to existing JavaScript object by declaring non-abstract non-native methods in overlay types. These methods, however, have additional restriction: they should not override methods of a parent class or interface. Please, note that the current version of TeaVM does not validate this, so you violate this restriction on your risk. Future versions of TeaVM will check this rule properly.
Example:
public interface HTMLElement extends JSObject {
@JSProperty
CSSStyleDeclaration getStyle();
// Subtypes can't override these methods
default void hide() {
getStyle().setProperty("display", "none");
}
default void show() {
getStyle().removeProperty("display");
}
}
Also, you can declare @JSBody
methods in abstract classes. Rewrite our example this way:
public abstract class HTMLElement implements JSObject {
@JSBody(script = "this.style.display = 'none';")
public native void hide();
@JSBody(script = "this.style.display = '';")
public native void show();
}
Static methods
Overlay classes can declare static methods, either with Java or JavaScript implementation. Example:
@JSClass("Array")
public class JSArray<T extends JSObject> implements JSObject {
public JSArray() {
}
// Does not exist in JS Array, implemented on Java side
public static <S extends JSObject> JSArray<S> of(
Collection<S> elements) {
var array = new JSArray<S>(elements.size());
for (int i = 0; i < elements.size(); ++i) {
array.set(i, elements.get(i));
}
return array;
}
// Wraps JS method Array.isArray
public static native boolean isArray(Object object);
}
Wrapping indexers
To access JavaScript objects as arrays or maps, you can declare indexer methods. Indexer methods are either get indexers or set indexers.
- getter indexers take one parameter and return a non-void value;
- setter indexers take two parameters; first is index, second is value to set;
You are free to name your indexers as your want.
To tell TeaVM that method is either get or set indexer, you should annotate it with @JSIndexer.
For example,
public interface Int8Array extends JSObject {
@JSIndexer
byte get(int index);
@JSIndexer
void set(int index, byte value);
//...
}
Passing Java objects to JavaScript
Some JavaScript APIs expect that you pass a callback object. You can simply implement JSObject interfaces in Java and pass these implementations to JavaScript wrappers. However, these implementation can only support method invocations, no properties, indexers and constructors.
For example, if you have
public interface Element extends JSObject {
//...
void addEventListener(String type, EventListener listener);
//...
}
public interface EventListener extends JSObject {
void handleEvent(Event evt);
}
you can do the following:
var window = Window.current();
Element element = window.getDocument().getElementById("my-elem");
element.addListener("click", evt -> window.alert(evt));
Passing Java objects as JavaScript functions
Often, JavaScript APIs expect you to pass a callback function. This case is similar to passing as JavaScript objects, however you need to tell TeaVM to pass your Java classes as JavaScript functions. To do this, simply add the @JSFunctor annotation. Functor interfaces must contain exactly one method.
For example,
@JSFunctor
public interface TimerHandler extends JSObject {
void onTimer();
}
@JSBody(params = { "handler", "delay" }, script = "setTimeout(handler, delay);")
static native void setTimeout(TimerHandler handler, int delay);
static void doWork() {
var doc = HTMLDocument.current();
setTimeout(() -> doc.getBody().appendChild(doc.createTextNode("-")), 1000);
}
Passing arrays without copying
By default, TeaVM copies all arrays in the gap between JavaScript and Java.
To override this behaviour, use @JSByRef
annotation on parameters or method.
For example:
@JSByRef
@JSBody(script = "return new Int32Array(10);")
private native int[] getArrayFromJS();
@JSBody(params = "array", script = "console.log(array.byteLength);")
private native void passArrayToJs(@JSByRef float[] array);
You should be careful when using @JSByRef
with return type. In Java arrays never overlap, but using
@JSByRef
you can make TeaVM violate this contract:
@JSByRef(params = "array", script = "return new Int8Array(array.buffer, 1);")
private native byte[] subarray(@JSByRef byte[] array);
This can have unexpected consequences and non-obvious errors. Please, avoid this!
Defining top-level functions and properties
You can also define top-level functions and properties using @JSTopLevel
with static
class methods.
For example:
public class Window {
@JSTopLevel
public static native String atob(String s);
@JSTopLevel
public static native String btoa(String s);
@JSTopLevel
@JSProperty
public static native HTMLDocument getDocument();
}
Importing declarations from module
You can import classes, functions and properties from external modules.
To do so, annotate corresponding elements with @JSModule
.
For example:
@JSClass
@JSModule("./myModule.js")
public class ImportedClass implements JSObject {
}
@JSClass
public class ImportedDeclarations implements JSObject {
@JSTopLevel
@JSModule("./myModule.js")
public static native void someFunction();
@JSTopLevel
@JSProperty
@JSModule("./myModule.js")
public static native String getSomeProperty();
}
Conversion rules
TeaVM automatically converts from and to JS following types:
boolean
,byte
,short
,int
,float
,double
which correspond to JavaScript numeric values.java.lang.String
which corresponds to JavaScriptString
object.- arrays of objects and primitives listed above.
TeaVM does not convert Java collections and primitive wrappers. Additionally, TeaVM only performs conversion when type is directly known from method's signature. This means that with generics you won't get expected results, because type arguments from generics only known at compile time.
In following example TeaVM is able to convert JS string to java.lang.String
:
private static void test() {
System.out.println(read());
}
@JSBody(script = "return document.getElementById('value-input').value;")
private static native String read();
however, with generics TeaVM will produce ClassCastException
on runtime:
private static void test() {
readAsync().then(value -> System.out.println(value));
}
private static native JSPromise<String> readAsync();
the right way to fix this is to declare JS wrapper as the type argument:
private static void test() {
readAsync().then(value -> System.out.println(value.stringValue()));
}
private static native JSPromise<JSString> readAsync();
Dynamic type casting
TeaVM only supports instanceof
against non-interface overlay types.
The reason is that there's no such thing as "interface" in JavaScript.
Some APIs in JavaScript declare that they consume or produce an object with given properties,
but this object should not necessarily extend some class.
Due to duck typing in JavaScript, there's no need to declare interfaces.
To express such APIs in statically typed Java, you can use interfaces, but these interfaces don't exist on runtime.
Additionally, you may want to express such "anonymous" JavaScript object with abstract classes.
In this case you can prevent TeaVM from inserting type checks for such classes
by adding @JSClass(transparent = true)
.
For example:
// `instanceof SomeClass` will always produce true
@JSClass(transparent = true)
public abstract class SomeClass {
public abstract void foo();
@JSProperty
public abstract String getBar();
}