DOM
SWT
Integration
Logging
IDE
Chromium features
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 8 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) {
URL url = ContentListening.class.getResource(resourceFile);
try (Scanner 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
:
Engine engine = Engine.newInstance(
EngineOptions.newBuilder(HARDWARE_ACCELERATED).build());
Browser 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(() -> {
BrowserView view = BrowserView.newInstance(browser);
JFrame 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 -> {
Frame frame = params.frame();
String 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:
- Obtain an instance of the JavaScript
window
object. - Create a Java object that listens to content changes and set it as a property named
java
in thewindow
. Earlier, in JavaScript code, we madeMutationObserver
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 -> {
String javaScript = load("observer.js");
event.frame().executeJavaScript(javaScript);
});
The remaining step is to load the page into the browser:
String html = load("index.html");
String base64Html = Base64.getEncoder().encodeToString(html.getBytes(UTF_8));
String 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) {
Engine engine = Engine.newInstance(
EngineOptions.newBuilder(HARDWARE_ACCELERATED).build());
Browser browser = engine.newBrowser();
SwingUtilities.invokeLater(() -> {
BrowserView view = BrowserView.newInstance(browser);
JFrame 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 -> {
Frame frame = params.frame();
String 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 -> {
String javaScript = load("observer.js");
event.frame().executeJavaScript(javaScript);
});
String html = load("index.html");
String base64Html = Base64.getEncoder().encodeToString(html.getBytes(UTF_8));
String dataUrl = "data:text/html;base64," + base64Html;
browser.navigation().loadUrl(dataUrl);
}
/**
* Loads a resource content as a string.
*/
private static String load(String resourceFile) {
URL url = ContentListening.class.getResource(resourceFile);
try (Scanner 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 thewindow
object. - The Java object that listens to DOM events.
- The Java code that adds a
InjectJsCallback
to aBrowser
instance, loads the web page, and makes theBrowser
instance execute the JavaScript code that makes theMutationObserver
pass changes to the listening Java object.