javajavafxjavafx-webview

JavaFX WebView call method on returned java object


I retrieve an object from Java to my JavaScript in WebView, and want to call methods on it. Doesn't work.

I have a JavaFX WebView, which displays an html page. This page includes JavaScript. I set a Java object as member variable on the "window", so that I can call methods on it from JavaScript, as per the documentation:

WebEngine engine = webView.getEngine();
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("manager", new Manager());

That's what I find in all examples. Assume the Manager class has a method, "doSomething()", I can call that method, on my Manager Java object, from JavaScript:

<script>
    function onclick() {
        manager.doSomething();
    }
</script>

This works just fine.

However, assume the doSomething() returns an object, e.g. a Todo which has a method "getDescription()", which just returns a string.

It should look like this:

<script>
    function onclick() {
        var todo = manager.doSomething();
        var desc = todo.getDescription();
        document.getElementById("description-label").innerHTML = desc;
    }
</script>

But that doesn't work for me. I cannot execute "todo.getDescription()". The WebView never prints any errors anywhere, as far as I can tell, no matter what I break, so it's tough to figure out the problem.

As I understand the documentation here https://docs.oracle.com/javafx/2/api/javafx/scene/web/WebEngine.html

with this explanation:

... a JavaRuntimeObject is created. This is a JavaScript object that acts as a proxy for the Java object, in that accessing properties of the JavaRuntimeObject causes the Java field or method with the same name to be accessed

My approach should work.

Eventually I just want to retrieve a collection of objects, and display their data in a table.

What am I doing wrong?


Solution

  • Calling Java from JavaScript

    screenshot

    The example:

    1. Creates a JavaFX Label and a WebView.
    2. Loads an HTML document.
    3. Associates a JSObject with the JavaFX app.
    4. The JavaFX app exposes a method that provides the label.
    5. JavaScript is invoked which gets the app provided by the JSObject.
    6. JavaScript gets the label from the app and the text from the label.
    7. JavaScript sets the copied label text into an element in the HTML document which is displayed by the WebView.

    Some things to note:

    1. Use current documentation on WebEngine (for JavaFX 19), don't use outdated documentation from JavaFX 2.

      • The outdated documentation does not explain how to use the WebEngine within a modular environment. The additional information on modularity will be critical for some applications.
    2. To access the WebView, use:

      requires javafx.web;
      
    3. To access the JSObject which bridges Java and the WebView, use:

      requires jdk.jsobject;
      
    4. To have your code accessible to JavaScript executing in WebView, use:

      opens <package-with-your-code> to javafx.web
      

      This is required because, internally, the WebEngine will use reflection on your code to allow JavaScript to call it.

    5. Make sure the document is loaded before performing actions on it.

    You don't (as far as I can tell) need to provide transitive access to the objects you return to the webview. For instance, in the JavaScript I call, app.getLabel().getText();, but just opening the package with my app to javafx.web is sufficient, I don't need to open the javafx.scene.control package to javafx.web. I'm not exactly sure why things work like that, but that appeared to be the case.

    This JavaScript will also work:

    var label = app.getLabel(); 
    document.getElementById('text-from-java').innerHTML = label.getText();
    

    But this next JavaScript will not work, because the type Label is not known to JavaScript, even though when you use the var form you can execute methods on the var which is a javafx.scene.control.Label:

    Label label = app.getLabel(); 
    document.getElementById('text-from-java').innerHTML = label.getText();
    

    com/example/bridge/BridgeApp.java

    package com.example.bridge;
    
    import javafx.application.Application;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.layout.VBox;
    import javafx.scene.web.WebEngine;
    import javafx.scene.web.WebView;
    import javafx.stage.Stage;
    import netscape.javascript.JSObject;
    
    public class BridgeApp extends Application {
        private final Label label = new Label("Text copied from JavaFX to WebView");
    
        private static final String HTML = // language=HTML
                """
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <title>Home</title>
                </head>
                <body>
                    <span id="text-from-java"></span>
                </body>
                </html>
                """;
    
        @Override
        public void start(Stage stage) {
            WebView webView = new WebView();
            WebEngine engine = webView.getEngine();
            engine.documentProperty().addListener((observable, oldValue, newDocument) -> {
                JSObject window = (JSObject) engine.executeScript(
                        "window"
                );
                window.setMember(
                        "app",
                        this
                );
                engine.executeScript(
                        "document.getElementById('text-from-java').innerHTML = app.getLabel().getText();"
                );
            });
            engine.loadContent(HTML);
    
            VBox layout = new VBox(
                    10,
                    label,
                    webView
            );
            layout.setPadding(new Insets(10));
    
            Scene scene = new Scene(layout);
            stage.setScene(scene);
    
            stage.show();
        }
    
        public Label getLabel() {
            return label;
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    module-info.java

    module com.example.bridge {
        requires javafx.controls;
        requires javafx.web;
        requires jdk.jsobject;
    
        opens com.example.bridge to javafx.graphics, javafx.web;
    }
    

    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.example</groupId>
        <artifactId>bridge</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>bridge</name>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <junit.version>5.8.2</junit.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>19</version>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-web</artifactId>
                <version>19</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.10.1</version>
                    <configuration>
                        <source>19</source>
                        <target>19</target>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    Calling JavaScript from Java, passing data in the executeScript call

    The example in the answer is just for demo purposes, to demonstrate how to call Java code from a WebView. The same result could be accomplished by executing a script directly on the webengine as shown below, but then it wouldn't be demonstrating calling Java from JavaScript as requested in the question.

    engine.executeScript(
        "document.getElementById('text-from-java').innerHTML = '" + label.getText() + "';"
    );