Sunday, October 27, 2013

AngularJS in a Desktop app pt. 1

Why AngularJS+FX?

(I am very sorry for formatting. Will search for another blogging platform)

Bear has an UI to monitor tasks being run on the hosts and write bear script snippets. When choosing a UI framework for the Bear, I considered several options which were JavaFX, AngularJS app with Play! framework backend or an AngularJS for Desktop via the WebView in JavaFX.

Why AngularJS+FX

  • Compared to the JavaFX app AngularJS+FX has Twitter Bootstrap (including many themes) and AngularJS. HTML provides a more rapid developing experience than FXML.
  • The most rapid development
  • No need to study yet another framework (JavaFX)
  • No external dependencies on the backend
  • Migrating to a server app should not be a pain
  • A nice experience being a pioneer :-) 
Now, after having a working prototype of an AngularJS Desktop app I'm thinking - why on Earth Oracle created JavaFX 2 why didn't Oracle create nice Java bindings for the WebView. Hiring a web developer to create UI for your desktop app could be cheaper and the result would be of better quality.

Binding your JS app to Java


Prerequisites: JDK 8 ea109+. Avoid using JDK 7 for the WebViews as it has Font rendering problems.

Below is the Hello World for JavaFX which is also demo of a bug in JavaFX (at Github). 

public class TestOverloadingApp extends Application {

    private WebEngine webEngine;

    @Override
    public void start(Stage stage) throws Exception {
        final WebView webView = new WebView();
        webEngine = webView.getEngine();

        Scene scene = new Scene(webView);

        stage.setScene(scene);
        stage.setWidth(1200);
        stage.setHeight(600);
        stage.show();

        // this is a proper way to load a page from your resources
        // if your testOverloading.html references an image with <img src="images/test.jpg">
        // then your test.jpg must be placed in /javafx/overloading1/images/test.jpg     
        webEngine.load(TestOverloadingApp.class.getResource("/javafx/overloading1/testOverloading.html").toURI().toURL().toString());

        webEngine.setOnAlert(new EventHandler<WebEvent<String>>() {
            @Override
            public void handle(WebEvent<String> stringWebEvent) {
                // this is a simple way to debug your app - call alert('this will into your Java log') on the JS side
                System.out.println("alert: " + stringWebEvent.getData());
            }
        });

        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> ov, Worker.State t, Worker.State t1) {
                System.out.println("[JAVA INIT] setting...");
                if (t1 == Worker.State.SUCCEEDED) {
                    // get the window object is a global variable in JS
                    JSObject window = (JSObject) webEngine.executeScript("window");

                    // bind our Java objects as fields in the window object
                    // in JS this would look like
                    // window.foo = new Foo() 
                    window.setMember("fooWhichIsOK", new FooWhichIsOk());
                    window.setMember("foo", new Foo());
                }
            }
        });
    }

    public static void main(String[] args) {
        launch(args);
    }   
}


HTML

In HTML you would call your provided Java instances:

<script>
function testOk(){
    // this invokes a method on a Java bean registered previously.
    window.foo.foo('test');
    // log an error
    alert('have called a method');
}
</script>

<body>
    <div id="okTest" onclick="testOk()">Run OK Test</div>
</body>

Current bindings limitations

After a number of tries I must say that the binding usage is quite limited. Below are the results of my work with JDK 8 ea109

Calling Java from JavaScript

  • Consider using only primitive types as parameters of java methods (i.e. int, char, String, arrays, String[])
  • For anything more complex parse JSON String
  • Overridden methods don't work
For JSON conversion I used facade beans which receive calls from JS and convert data to Java. It's similar to Controllers in frameworks like Grails or AngularJS.

Calling JavaScript from Java

This is pretty straight-forward. Just call it like this

webEngine.evaluate(window.yourObject.receiveJSON('{your:"JSON object here", with: "your params"}'))

Firebug Lite

You will soon notice that WebView lacks the development tools. To enable FirebugLite, add this script to your HTML:

<script src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'>

This is the only version which worked for me.

Init order

Init order is the following.
  1. Document is loaded into the WebView.
  2. $(document).ready(...) is called inside the WebEngine
  3. JavaFX change listener is called.
So your JavaFX init code is always called the last. Below is an example of how this could done.

var Java = {};

JS:
Java.init = function(window){
    Java.log("initializing Java...");
    Java.OpenBean = window.OpenBean;  //registers the bindings
};

Java.mode = navigator.userAgent.match(/Chrome\/\d\d/) ?
    'Chrome' :
    (navigator.userAgent.match(/Firefox\/\d\d/) ? 'FF' : 'FX');

Java.isFX = Java.mode === 'FX';

// this one could be merged into the previous
Java.initApp = function(){
    if(this.initialized){
        return;
    }
 
    console.log("started Java.initApp...");

    this.initialized = true;

    // further app initialisation
}

// One can mock this initApp call with jQuery.ready
$(document).ready(function(){
    Java.initApp();
});

Java:
webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener() {
    @Override
    public void changed(ObservableValue ov, Worker.State t, Worker.State t1) {
    logger.info("[JAVA INIT] setting...");

    if (t1 == Worker.State.SUCCEEDED) {
        logger.info("ok");
        JSObject window = (JSObject) webEngine.executeScript("window");
        window.setMember("bearFX", bearFX);
        window.setMember("Bindings", bindings);

        logger.info("[JAVA INIT] calling bindings JS initializer...");
        webEngine.executeScript("Java.init(window);");
        logger.info("[JAVA INIT] calling app JS initializer...");
        webEngine.executeScript("Java.initApp();");
    }
    }
});


Source is available at Github

BearFX.java

Update for Kostya:

I'm being blocked by my russian censorship firewall on blogspot.ru (and I'm editing at blogspot.com), so I'll answer here - not, I haven't tried that.

Hello guys

Hi guys, I am Andrey and this is my professional blog. I am a 28 y.o. Java developer from Saint-Petersburg, a nice city in Russia overall, a bit frosty for me, so I am planning to relocate.

In this blog I will write about Java and friends. The very first topics of my blog will all be devoted my current open source project called Bear.