JsInterop - fail to iterate @JsType in List

72 views
Skip to first unread message

Freddy Boucher

unread,
May 12, 2020, 3:14:08 AM5/12/20
to GWT Users
Hi,

So I don't know if it's a known JsInterop issue but here is my problem (PS: I'm running GWT 2.9.0-RC1):

So in my home page I have the following code:

<script>
class Car {
start() {
return "start";
}
}

var car = new Car();
</script>

I have the following @JsType:

import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;

@JsType(isNative = true)
public class Car {
@JsProperty(namespace = JsPackage.GLOBAL) public static Car car;

public native String start();
}

And I'm just trying the following code:

GWT.log("direct call: " + Car.car.start());
List<Car> cars = Arrays.asList(Car.car);
try {
for (Car car : cars) {
car.start();
}
} catch (Exception exception) {
GWT.log("FAILED to iterate a @JsType in a List", exception);
}

But it fails with the following logs:
direct call: start
ConsoleLogger.java:33 FAILED to iterate a @JsType in a List
ConsoleLogger.java:55 Exception: com.google.gwt.core.client.JavaScriptException: (TypeError) : Cannot read property 'project' of undefined
ConsoleLogger.java:33 TypeError: Cannot read property 'project' of undefined
at dse_g$ (App.java:75)
at tse_g$.use_g$ [as execute_1_g$] (App.java:66)
at rfb_g$ (SchedulerImpl.java:50)
at Xeb_g$ (SchedulerImpl.java:183)
at Meb_g$.Seb_g$ [as flushPostEventPumpCommands_0_g$] (SchedulerImpl.java:347)
at ifb_g$.jfb_g$ [as execute_2_g$] (SchedulerImpl.java:78)
at Peb_g$ (SchedulerImpl.java:141)
at ydb_g$ (Impl.java:309)
at Bdb_g$ (Impl.java:361)
at Impl.java:78
at callback_0_g$ (SchedulerImpl.java:196)


I have a small demo project to reproduce the issue: https://github.com/freddyboucher/gwt-storage-objectify/commit/e4184b0ccd5bd490b16cbc6bb56b0d33fe6e76d1

Is it a bug or a known limitation of JsType?

Thank you

Jens

unread,
May 12, 2020, 3:28:27 AM5/12/20
to GWT Users

But it fails with the following logs:
direct call: start
ConsoleLogger.java:33 FAILED to iterate a @JsType in a List
ConsoleLogger.java:55 Exception: com.google.gwt.core.client.JavaScriptException: (TypeError) : Cannot read property 'project' of undefined
ConsoleLogger.java:33 TypeError: Cannot read property 'project' of undefined
at dse_g$ (App.java:75)
at tse_g$.use_g$ [as execute_1_g$] (App.java:66)

Where does 'project' come from? Looks like the exception does not match the example code?

-- J. 

Freddy Boucher

unread,
May 12, 2020, 3:33:22 AM5/12/20
to GWT Users
Hi @Jens

Can be project referring to my package name?
But I confirm the log is correct and it's produced by this exact sample

package com.project.client;

import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;

@JsType(isNative = true)
public class Car {
 @JsProperty(namespace = JsPackage.GLOBAL) public static Car car;

  public native String start();
}

Freddy Boucher

unread,
May 12, 2020, 3:55:58 AM5/12/20
to GWT Users
So I updated my sample project to remove the try/catch so it will throw the JavaScriptException

Logger.getLogger("").info("direct call: " + Car.car.start());
List<Car> cars = Arrays.asList(Car.car);
for (Car car : cars) {
  car.start();
}
Logger.getLogger("").info("It never goes here!!");

So here is the code that I've just deployed: https://github.com/freddyboucher/gwt-storage-objectify/commit/b0ad562f86deb09ee6bf556b32798116e627f347
And you can experience the issue here: https://jsinterop-dot-gwt-storage-objectify.appspot.com/?compiler.stackMode=emulated

Jens

unread,
May 12, 2020, 5:14:33 AM5/12/20
to GWT Users
I am not 100% sure since I have not used JsInterop for a long time now but I would imagine the following:

Currently GWT will think that your native JS class "Car" is located at com.project.client namespace because that is the package it lives in and you have not provided a namespace to @JsType.

You can access the static car field because you have added the global namespace to @JsProperty and your custom JS has created a car field in that global namespace. In your for-loop I am relatively sure that GWT will try to verify that your Java list contains actual JS Cars so the generated JS will somewhere contain "$wnd.com.project.client.Car" which is undefined since your JS did not define it (you have defined everything in the global space, including the class itself)

So you should use @JsType(isNative = true, namespace = JsPackage.GLOBAL) on your Car class to tell GWT to use $wnd.Car since that is the location where you have defined that class in JS.


-- J.

Freddy Boucher

unread,
May 12, 2020, 5:20:45 AM5/12/20
to GWT Users
Thanks @Jens

So I added namespace = JsPackage.GLOBAL as suggested:

package com.project.client;

import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;

@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public class Car {
 @JsProperty(namespace = JsPackage.GLOBAL) public static Car car;

  public native String start();
}

But the new stacktrace is:

SEVERE:
java.lang.ClassCastException
  at Unknown.xk_g$(Throwable.java:69)
  at Unknown.el_g$(Exception.java:29)
  at Unknown.ml_g$(RuntimeException.java:29)
  at Unknown.BXe_g$(ClassCastException.java:27)
  at Unknown.dyg_g$(InternalPreconditions.java:154)
  at Unknown.pyg_g$(InternalPreconditions.java:138)
  at Unknown.oyg_g$(InternalPreconditions.java:133)
  at Unknown.c9c_g$(Cast.java:155)
  at Unknown.ose_g$(App.java:74)
  at Unknown.Fse_g$(App.java:66)
  at Unknown.rfb_g$(SchedulerImpl.java:50)
  at Unknown.Xeb_g$(SchedulerImpl.java:183)
  at Unknown.Seb_g$(SchedulerImpl.java:347)
  at Unknown.jfb_g$(SchedulerImpl.java:78)
  at Unknown.Peb_g$(SchedulerImpl.java:141)
  at Unknown.ydb_g$(Impl.java:309)
  at Unknown.Bdb_g$(Impl.java:361)
  at Unknown.anonymous(Impl.java:78)

Other ideas?

Jens

unread,
May 12, 2020, 5:47:05 AM5/12/20
to GWT Users

Other ideas?

Maybe because you have used a ES6 / ECMAScript2015 class in your custom JS instead of a traditional function() based JS class. For example web components also use ES6 classes and it is not straight forward to use them with JsInterop / GWT.

I would start SDM / Compiler with -style PRETTY and look at / debug the JS to see why a cast exception occurs or try define the class without using ES6.

-- J.


Freddy Boucher

unread,
May 12, 2020, 6:14:50 AM5/12/20
to GWT Users
@Jens

Good catch! It does work with the following javascript:

  <script>
   function Car() {
     this.start = function () {
       return "start";
     }
   }

    var car = new Car();
 </script>

But as you guessed, this is just some dummy code, my real javascript is the Microsoft Monaco Editor and of course I don't have any control on it.
What is the recommended approach in that case?

Thank you

Tino D.

unread,
May 12, 2020, 6:32:37 AM5/12/20
to GWT Users
I think you can achieve this by using Babel: https://babeljs.io/ . A lot of JS-Libs use this for shipping and "Legacy"-support. See also: https://medium.com/hired-engineering/setting-up-monaco-with-jest-e1e4c963ac

Colin Alworth

unread,
May 12, 2020, 9:36:00 AM5/12/20
to GWT Users
Another option could be to tell GWT that this is just the idea of what the Car type will be, but that there isn't actually a real type called Car that it will be able to find. For example, you could perhaps make this an interface instead of a class (js has no actual interface types, just "it quacks, so its a duck"). Another option (but not a good one) is to further modify the `@JsType` to have `name="Object", so that GWT ignores all type checks. This will get weird in j2cl though, as you'll need externs that reflect this code style (though you'll need externs anyway, if monaco isn't capable of being fed to closure-compiler).

I do believe that the es2015 class keyword emits a constructor that GWT can work with though - the limitations for GWT and WebComponents are that you can't "extend" a es2015 class using prototype, and GWT always uses prototype.

If the script block does some scoping (modules perhaps?) that could hide the Car type from GWT, so that it is unable to do the JS `(c instanceof $wnd.Car)` check required to cast the element when it comes out of the generic ArrayList<Car>.get().

Freddy Boucher

unread,
May 13, 2020, 11:25:29 PM5/13/20
to GWT Users
@Colin

Ok so it does work using an interface! I just cannot declare a static field public static Car car in the interface but introducing a Global class to hold it does the trick!

@JsType(isNative = true)
public class Global {
 @JsProperty(namespace = JsPackage.GLOBAL) public static Car car;

  @JsType(isNative = true)
 public interface Car {
   String start();
 }
}

And for reference I kept:

  <script>
   class Car {
     start() {
       return "start";
     }
   }

    var car = new Car();
 </script>


I really thought I could either use interface or class depending on the best approach to map it to Java.

Anyway to come back to my real code, I was trying to define with JsInterop the Monaco Editor interface: IStandaloneCodeEditor

export interface IStandaloneCodeEditor extends ICodeEditor {
   updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void;
   addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null;
   createContextKey<T>(key: string, defaultValue: T): IContextKey<T>;
   addAction(descriptor: IActionDescriptor): IDisposable;
}

So I had this JsInterop mapping:

    @JsType(isNative = true)
   public interface IStandaloneCodeEditor extends ICodeEditor {
       String addCommand(@Nonnull KeyCode keyCode, @Nonnull Function handler, @Nullable String context);
   }


But then I wanted to access a private field so I converted my interface to a class and that's where all my troubles came from:

    @JsType(isNative = true)
   public abstract class IStandaloneCodeEditor implements ICodeEditor {
       @JsProperty
       KeybindingService _standaloneKeybindingService;

        public native String addCommand(@Nonnull KeyCode keyCode, @Nonnull Function handler, @Nullable String context);
   }

So at the end, I have to keep it declared as interface and everything works fine now!

    @JsType(isNative = true)
   public interface IStandaloneCodeEditor extends ICodeEditor {
       @JsProperty(name = "_standaloneKeybindingService")
       KeybindingService _standaloneKeybindingService();

        String addCommand(@Nonnull KeyCode keyCode, @Nonnull Function handler, @Nullable String context);
   }

@Tino
Thanks for the Babel approach but as you see in this comment, my repro code was not even close to my real issue. My real issue has nothing to do with es2015 class keyword ;)

Thanks all
Reply all
Reply to author
Forward
0 new messages