Skip to content

Latest commit

 

History

History
418 lines (332 loc) · 9.77 KB

jsinterop-by-example.md

File metadata and controls

418 lines (332 loc) · 9.77 KB

J2CL JsInterop by Example

This is meant to be a quick guide on how to use JsInterop. More details can be found in the design doc.

Calling JavaScript from Java

For calling browser APIs you may want to consider Elemental which has autogenerated and accurate JsInterop abstractions for (common) JavaScript APIs.

@JsType - Interface with Closure utilities.

// JsInterop for goog.string.linkify
class ClosureLinkify {
  @JsMethod(namespace="goog.string.linkify")
  public static native String findFirstEmail(String text);
  @JsMethod(namespace="goog.string.linkify")
  public static native String findFirstUrl(String text);
}

Example usage:

// Main.java
ClosureLinkify.findFirstEmail("this is an email: [email protected]");
// Main.impl.java.js (transpiled from Main.java).
const Linkify = goog.require("goog.string.linkify");
Linkify.findFirstEmail("this is an email: [email protected]");

@JsType - Interface with a Closure type. {#closure_type}

Note that @JsType is just syntactical sugar for applying @JsConstructor, @JsMethod or@JsProperty to all public constructors, methods and properties respectively.

@JsType(isNative=true, namespace="goog.html.sanitizer")
public class HtmlSanitizer {
  public HtmlSanitizer(Builder builder){};
  public native SafeHtml sanitize(String unsanitizedHtml);
  public native HTMLSpanElement sanitizeToDomNode(String unsanitizedHtml);

  public static native SafeHtml sanitize(String unsanitizedHtml);

  // Note: The inner class without namespace inherits the parent as namespace.
  @JsType(isNative=true)
  public static class Builder {
    public native Builder allowCssStyles();
  }
}

// Usage
static void main() {
  HtmlSanitizer.Builder builder = new HtmlSanitizer.Builder();
  builder.allowCssStyles();
  SafeHtml html = new HtmlSanitizer(builder).sanitize("<div />");
}

@JsType - Interface with non top-level exports and TypeScript types.

Similar to interfacing with Closure type with inner classes. However if you don't want to declare a Java type for the top level export, you can use 'dot' notation when specifying the type's name.

@JsType(isNative=true, name="bar.Foo" namespace="path.for.directory.of.type")
public class Foo {}

@JsType - Interface with an extern type.

@JsType(isNative=true, namespace=JsPackage.GLOBAL)
public class RegExp {
  public RegExp(String pattern, String... flags) {}
  public native boolean test();
  public String flags;
}

// Usage
static void main() {
  RegExp re = new RegExp("asdf", "g");
  String flags = re.flags; // "g"
  re.test("hello");
}

@JsFunction - Implement JavaScript callbacks in Java

class Main {

  @JsFunction
  interface Callback {
    void run();
  }

  // Global JavaScript method setTimeout().
  @JsMethod(namespace=JsPackage.GLOBAL)
  private static native int setTimeout(Callback callback, int delayMs);

  public static void main() {
    setTimeout(() -> {
      // do something!
    }, 1000);
  }
}

@JsEnum - Interface with an existing Closure enum

// Existing ClosureEnum.js
/** @enum {string} */
const ClosureEnum = {
  OK : 'Ok',
  CANCEL : 'Cancel'
}
// Handwritten Java code to expose ClosureEnum to Java code.
@JsEnum(isNative=true)
enum ClosureEnum {
  OK,
  CANCEL
}

You can add String value field and set hasCustomValue=true if you would like get the underlying String value of the enum.

Calling Java from JavaScript

Java transpiled by J2CL is not directly callable from JavaScript by default. J2CL Java classes have mangled method and property names to preserve Java semantics in the translated code.

Mangled names are not public APIs for Java classes and the mangling scheme is subject to change. Instead, JsInterop annotations are the safe way to provide JavaScript APIs.

@JsConstructor - Instantiate a Java class from JavaScript.

// MyClass.java
package com.google.example;

class MyClass {
  @JsConstructor
  public MyClass() {}
}
// User written JavaScript code using J2CL transpiled MyClass.java.
goog.module("example.module");
const MyClass = goog.require("com.google.example.MyClass");

new MyClass();

@JsMethod

// Interop.java
package com.google.example;

class Interop {
  @JsMethod
  public void jsMethod() {}
  // Cannot call this from JS.
  public void notJsMethod() {}
}
// Interop.impl.java.js (transpiled from Interop.java)
goog.module("com.google.example.Interop");

class Interop {
  jsMethod() {}
  // This is mangled. Don't call it. The name is subject to change.
  m_notJsMethod__() {}
}

exports = Interop;

@JsProperty

// Interop.java
package com.google.example;

public class Interop {
  @JsProperty
  public static final int staticProp = 10;

  @JsProperty
  public int instanceProp = 30;

  // Getter for a JavaScript property named "instance"
  @JsProperty
  public static Interop getInstance() {
    return new Interop();
  }
}
// User written JavaScript code using J2CL transpiled Interop.java.
goog.module("com.google.example.Usage");

const Interop = goog.require("com.google.example.Interop")

console.log(Interop.staticProp); // 10
console.log(Interop.instance.instanceProp); // 30

@JsOptional - Closure optional parameters

See Optional Types in Closure Compiler.

// Main.java
public class Main {
  @JsMethod
  public void method1(@JsOptional @Nullable Double i) {}
}
// Main.impl.java.js (transpiled from Main.java)
class Main {
  /**
   * @param {number=} i // Note the "=" here.
   * @return {void}
   * @public
   */
  method1(i) {}
}

Note: It is generally a good practice to mark @JsOptional explicitly @Nullable to catch potential problems with Java nullabilty checkers.

@JsType - Expose an entire class to JavaScript

// Exported.java
package com.google.example;

@JsType
public class Exported {
  public int prop;
  public Exported() {
    this.prop = 100;
  }
  public int value() {
    return this.prop;
  }
}
// User written JavaScript code using J2CL transpiled Exported.java.
goog.module("com.google.example.usage");
const Exported = goog.require("com.google.example.Exported");

let e = new Exported();
console.log(e.prop); // 100
console.log(e.value()); // 100

@JsEnum - Expose a Java enum to JavaScript

See Enum Types in Closure

@JsEnum
enum ExposedEnum {
  VALUE1,
  VALUE2;

  void someMethod();
}
// ExposedEnum.impl.java.js (transpiled from ExposedEnum.java)
/** @enum {number} */
const ExposedEnum = {
  VALUE1 : 0,
  VALUE2 : 1
}
// Note that no methods from original Java source are exposed here.

Advanced topics

Subclassing a class with overloaded methods

// Main.java
public class Main {
  @JsMethod
  public void method(@JsOptional @Nullable Object o) {
    Console.log("Called method() with " + o);
  }
  // Alias method(@JsOptional @Nullable Object o). Calling method() (from Java code) is
  // equivalent to calling method(null).
  @JsMethod
  public native void method();

  private void main() {
    method("Hello"); // logs to console "Called method() with Hello"
    method(); // logs to console "Called method() with null"
  }
}

// SubMain.java
public class SubMain extends Main {
  @JsMethod
  public void method(@JsOptional @Nullable Object o1, @JsOptional @Nullable Object o2) {
    Console.log("Called method(Object, Object) with " + o1 + " " +  o2);
  }

  // Override the original implementation with a native method to make sure
  // there is still single implementation of the method in the class.
  @JsMethod
  public native void method(@JsOptional @Nullable Object o);

  private void main() {
    method("Hello", "GoodBye"); // logs to console "Called method() with Hello GoodBye"
    method("Hello"); // logs to console "Called method() with Hello null"
    method(); // logs to console "Called method() with null null"
  }
}

Exposing a Java enum with custom value to JavaScript

@JsEnum(hasCustomValue=true)
enum ExposedEnum {
  VALUE1("Value1"),
  VALUE2("Value2");

  // An instance field named 'value' defines the custom value type.
  String value;

  // A single constructor with this shape is needed to define custom values.
  ExposedEnum(String value) {
    this.value = value;
  }
}
// ExposedEnum.impl.java.js (transpiled from ExposedEnum.java)
/** @enum {?string} */
const ExposedEnum = {
  VALUE1 : 'Value1',
  VALUE2 : 'Value2'
}

Calling legacy Closure functions that are themselves a namespace

// Some old existing bar.js
goog.provide('foo.bar');

/** @return {string} */
foo.bar = function(value) {
  return 'bar';
};
// Handwritten Java code to expose foo.bar to Java code.
public class Main {
  // Note that the method sets the namespace and leaves the name empty.
  @JsMethod(namespace = "foo.bar", name = "")
  public static native String bar();
}

Accessing legacy Closure properties that are themselves a namespace

// Some old existing bar.js
goog.provide('foo.bar');

/** @return {string} */
foo.bar = 'bar';
// Handwritten Java code to expose foo.bar to Java code.
public class Main {
  // Note that the method sets the namespace and leaves the name empty.
  @JsProperty(namespace = "foo.bar", name = "")
  public static native String bar();
}