List icon Contents

Listening to DOM content changes

This tutorial shows how to build a small Java application that listens to changes occurred in an HTML page loaded into JxBrowser.

Prerequisites

To go through this tutorial you will need:

  • Git.
  • Java 17 or higher.
  • A valid JxBrowser license. It can be either Evaluation or Commercial. For more information on licensing please see the licensing guide.

Setting up a project

The code of the example application for this tutorial is available along with other examples from a GitHub repository as a Gradle-based project.

If you want to build a Maven-based project, please refer to the Maven config guide. If you would like to build a Gradle-based project from scratch, please see the Gradle config guide.

Getting the code

To get the code please execute the following commands:

$ git clone https://github.com/TeamDev-IP/JxBrowser-Examples
$ cd JxBrowser-Examples/tutorials/content-changes

Now we are in the root directory of all examples. The code of this tutorial is under the tutorials/content-changes directory.

Adding the license

To run this tutorial you need to set up a license key.

The page

We are going to load a simple HTML page on which we will display a counter auto-incrementing every second.

Here is the code of the counter:

<div>
  <span class="counter" id="counter"></span>
</div>

The counter is updated using jQuery:

<script crossorigin="anonymous" src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        var counter = 1;
        setInterval(function() { $(".counter").text(counter++); }, 1000);
    });

</script>

Here is the full code of the page, which is included into the project as the resource file named index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>index</title>
</head>
<body>
<div>
  <span class="counter" id="counter"></span>
</div>

<script crossorigin="anonymous" src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        var counter = 1;
        setInterval(function() { $(".counter").text(counter++); }, 1000);
    });

</script>
</body>
</html>

The JavaScript code

As we want to notify the Java code of our application from within our HTML code, we need the JavaScript code that handles DOM modifications and passes them on to our Java code. We could have done it right in the HTML code, but we want to show how to add such a code dynamically from Java.

First, we obtain the element to track:

const element = document.getElementById('counter');

Then we create a MutationObserver instance with a callback to pass the data to the Java code.

const observer = new MutationObserver(
    function(mutations) {
        window.java.onDomChanged(element.innerHTML);
    });

The most important part here is this call:

window.java.onDomChanged(element.innerHTML);

Here we reference an object stored in the window object as the property named java is the name of the property which contains the Java object to be called. We use the word java to highlight the fact that we are calling a Java object. It can be any JavaScript identifier that matches the sense of your application.

The method of the object that we are calling is onDomChanged(). Later we will add this method when we create a Java class for listening to content changes. We pass innerHTML property of the counter element. So, the method will accept a String parameter.

Now, let’s tell the observer to track DOM changes:

const config = {childList: true};
observer.observe(element, config);

Here is the full JavaScript code that we put in resources as observer.js file:

const element = document.getElementById('counter');
const observer = new MutationObserver(
    function(mutations) {
        window.java.onDomChanged(element.innerHTML);
    });
const config = {childList: true};
observer.observe(element, config);

The Java code

Utility for loading resources

In previous sections we reviewed HTML and JavaScript code stored as resources. Now we need the code that loads them. We will use the simplest approach using the Resources utility class from Guava.

Here is the code of the utility method load() that we will use later:

private static String load(String resourceFile) {
    var url = ContentListening.class.getResource(resourceFile);
    try (var scanner = new Scanner(url.openStream(),
            Charsets.UTF_8.toString())) {
        scanner.useDelimiter("\\A");
        return scanner.hasNext() ? scanner.next() : "";
    } catch (IOException e) {
        throw new IllegalStateException("Unable to load resource " +
                resourceFile, e);
    }
}

Creating Browser and BrowserView

For the sake of simplicity of the example we will put all the code under the main() method. A real application would have more structured code.

First of all, we need to create an Engine and Browser:

var engine = Engine.newInstance(
        EngineOptions.newBuilder(HARDWARE_ACCELERATED).build());
var browser = engine.newBrowser();

Then in the Swing EDT we create a BrowserView and JFrame. Then we add the newly created BrowserView to the frame.

SwingUtilities.invokeLater(() -> {
    var view = BrowserView.newInstance(browser);

    var frame = new JFrame("Content Listening");
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    frame.add(view, BorderLayout.CENTER);
    frame.setSize(700, 500);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
});

Now we need to create an object that listens to DOM changes.

Java Object for listening DOM changes

As you may remember, the JavaScript code we reviewed earlier needed an object with the method named onDomChanged() which accepts String argument.

Here is the class:

public static class JavaObject {

    @SuppressWarnings("unused") // Invoked by the callback processing code.
    @JsAccessible
    public void onDomChanged(String innerHtml) {
        System.out.println("DOM node changed: " + innerHtml);
    }
}

Now we need to make JavaScript code to talk to our Java object. Let’s do it.

Wiring JavaScript and Java

In order to make JavaScript code talk to our Java object, we will implement a InjectJsCallback passing the instance to Browser.set().

browser.set(InjectJsCallback.class, params -> {
    var frame = params.frame();
    var window = "window";
    JsObject jsObject = frame.executeJavaScript(window);
    if (jsObject == null) {
        throw new IllegalStateException(
                format("'%s' JS object not found", window));
    }
    jsObject.putProperty("java", new JavaObject());
    return Response.proceed();
});

In this code we:

  1. Obtain an instance of the JavaScript window object.
  2. Create a Java object that listens to content changes and set it as a property named java in the window. Earlier, in JavaScript code, we made MutationObserver to pass data the object associated with this property.

Then we should register FrameLoadFinished event listener that will load the JavaScript code, the one with the MutationObserver arrangement, and make the Browser execute the code, finishing the wiring between JavaScript and Java when the DOM model is ready.

browser.navigation().on(FrameLoadFinished.class, event -> {
    var javaScript = load("observer.js");
    event.frame().executeJavaScript(javaScript);
});

The remaining step is to load the page into the browser:

var html = load("index.html");
var base64Html = Base64.getEncoder().encodeToString(html.getBytes(UTF_8));
var dataUrl = "data:text/html;base64," + base64Html;
browser.navigation().loadUrl(dataUrl);

Full Java code

That is it. Here is the complete Java code:


import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.Charsets;
import com.teamdev.jxbrowser.browser.Browser;
import com.teamdev.jxbrowser.browser.callback.InjectJsCallback;
import com.teamdev.jxbrowser.browser.callback.InjectJsCallback.Response;
import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.engine.EngineOptions;
import com.teamdev.jxbrowser.frame.Frame;
import com.teamdev.jxbrowser.js.JsAccessible;
import com.teamdev.jxbrowser.js.JsObject;
import com.teamdev.jxbrowser.navigation.event.FrameLoadFinished;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import java.awt.BorderLayout;
import java.io.IOException;
import java.net.URL;
import java.util.Base64;
import java.util.Scanner;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

/**
 * This example demonstrates how to listen to DOM changes from a Java object.
 */
public final class ContentListening {

    public static void main(String[] args) {
        var engine = Engine.newInstance(
                EngineOptions.newBuilder(HARDWARE_ACCELERATED).build());
        var browser = engine.newBrowser();
        SwingUtilities.invokeLater(() -> {
            var view = BrowserView.newInstance(browser);

            var frame = new JFrame("Content Listening");
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.add(view, BorderLayout.CENTER);
            frame.setSize(700, 500);
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });

        browser.set(InjectJsCallback.class, params -> {
            var frame = params.frame();
            var window = "window";
            JsObject jsObject = frame.executeJavaScript(window);
            if (jsObject == null) {
                throw new IllegalStateException(
                        format("'%s' JS object not found", window));
            }
            jsObject.putProperty("java", new JavaObject());
            return Response.proceed();
        });

        browser.navigation().on(FrameLoadFinished.class, event -> {
            var javaScript = load("observer.js");
            event.frame().executeJavaScript(javaScript);
        });

        var html = load("index.html");
        var base64Html = Base64.getEncoder().encodeToString(html.getBytes(UTF_8));
        var dataUrl = "data:text/html;base64," + base64Html;
        browser.navigation().loadUrl(dataUrl);
    }

    /**
     * Loads a resource content as a string.
     */
    private static String load(String resourceFile) {
        var url = ContentListening.class.getResource(resourceFile);
        try (var scanner = new Scanner(url.openStream(),
                Charsets.UTF_8.toString())) {
            scanner.useDelimiter("\\A");
            return scanner.hasNext() ? scanner.next() : "";
        } catch (IOException e) {
            throw new IllegalStateException("Unable to load resource " +
                    resourceFile, e);
        }
    }

    /**
     * The object observing DOM changes.
     *
     * <p>The class and methods that are invoked from JavaScript code must be public.
     */
    public static class JavaObject {

        @SuppressWarnings("unused") // invoked by callback processing code.
        @JsAccessible
        public void onDomChanged(String innerHtml) {
            System.out.println("DOM node changed: " + innerHtml);
        }
    }
}

If you run this program, you should see the browser window with the counter and the console output following the change in the browser window.

Summary

In this tutorial we create a small Java application that listens to DOM changes in a loaded web page.

The application consists of the following parts:

  • The HTML page with a DOM element which is going to change, and for which we know the ID.
  • The JavaScript code, which uses MutationObserver for notifying a Java object associated with the window object.
  • The Java object that listens to DOM events.
  • The Java code that adds a InjectJsCallback to a Browser instance, loads the web page, and makes the Browser instance execute the JavaScript code that makes the MutationObserver pass changes to the listening Java object.