Blog

Building Java API for JavaScript libraries - The lightweight approach

By  
Matti Tahvonen
Matti Tahvonen
·
On Sep 5, 2023 4:20:17 PM
·

Many Vaadin add-ons are wrappers around existing JavaScript libraries or components. The tooling and documentation in Vaadin are primarily targeted for the optimal case, where the wrapped component is a Web Component. The huge rewrite of the framework in version 10 was done largely to optimize this case. The reality, still in 2023, is that they often are not. But no worries, integrating to raw JS widgets can be very easy as well and lightweight for both the add-on developers and the Vaadin toolchain.

A common approach many Vaadin developers adopt has been creating a Web Component just for the sake of Vaadin integration and then utilizing the third-party JS library. An alternative is to skip all that part, inject the JS library directly into the application, and use the Element API in Vaadin Flow to make calls to the library. Thanks to very welcome enhancements in Java (namely text block), this approach is nowadays more tempting even for bit larger integrations.

A great little bonus in this approach is that nothing needs to be added to the front-end bundle of the actual applications. Thus, even after adding this kind of add-on to your Vaadin project, the “pre-compiled front-end bundle” introduced in Vaadin 24 is enough (read: faster builds, no need for npm for those using the add-on).

Injecting the script file dynamically

As discussed in the Vaadin documentation, the de facto way to inject JS is to use the @JsModule annotation. The good and bad parts of that approach are that the injected scripts will be included in the “front-end bundle,” which is then optimized by Vaadin at build time. The front-end bundle surely becomes optimized, but on the other hand, the optimization takes a considerable amount of time during the builds and requires front-end tooling. Ideally, the front-end tooling gets installed automatically by Vaadin, but this can be an issue in, e.g., corporate environments with strict firewalls.

An alternative to the @JsModule is the @JavaScript annotation. Instead of bundling these JS files, Vaadin will inject them into the host page directly just before the component is used. Using components built this way will give a better developer experience for Java developers, but the front-end code might be slightly less optimal.

As an add-on developer, you can serve the JS through, e.g., CDN or embed that into your add-on. The nasty thing about declarative annotations is that those cannot easily be overridden. Thus, my final suggestion for lightweight JS integrations is to inject the script programmatically. This way, you can provide a decent default, and add-on users can still override that in case they want, for example, a different version of the script or want it to be served from a custom location.

In this code snippet, you can see a tiny logic that avoids double-loading the JS library. That can be required for some libraries, but you can do just fine without it in most cases. 

Java components without Web Components

Now that the JavaScript library is injected into the host page using the lightweight approach (regarding the developer experience), the next step is to use it with minimal hassle. Even though the core components in the Vaadin design system are implemented using Web Components, Vaadin by no means requires you to have that as a counterpart for your Java component. Instead, we will use the Element API to call JS directly on basic HTML elements, such as div.

For components that will only be used in your own projects, it can be easiest to extend, for example, the built-in Div component:

public class MapLibre extends Div

This is handy but brings in a lot of API that you might not want in your add-on. For example, it makes no sense at all to add other components to many JS widgets, so for example, the inherited add(Component) would simply be broken and confuse other users. For more sophisticated cases, it is probably best to start with the Component class and define the tag name to be used on the client side:

@Tag(“div”’)
public class MapLibre extends Component

Extending the Component class brings in a minimal amount of public API. In most cases, it still makes sense to implement at least HasSize and HasStyle interfaces, whose default methods usually require no modification.

Inside the actual components, you can then leverage the low-level Element API to integrate with the underlying JS library. For example, with the MapLibre GL JS library I’m using here, rendering a trivial MapLibreJS example (without any actual Java API) could be as simple as this:

public MapLibreComponent() {
   loadMapLibreJs();

   // Fixed component size is needed for many JS components

   setHeight("400px");setWidth("600px");
   setId("map");

   // This is char-to-char copy from
   // https://maplibre.org/maplibre-gl-js/docs/

   getElement().executeJs("""
       var map = new maplibregl.Map({
           container: 'map',
           style: 'https://demotiles.maplibre.org/style.json', // style URL
           center: [0, 0], // starting position [lng, lat]
           zoom: 1 // starting zoom
       });
   """);
}

Passing in parameters

The default way to pass parameters for JS calls in Vaadin is to pass them in the varargs array that comes after the script body itself. This way, you can pass in basic data types and Element references. For example, to enhance the above example (and make multiple maps per page work), we can get rid of the id-based initialization like this:

getElement().executeJs("""

   var map = new maplibregl.Map({

       container: $0, // this is the first parameter in executeJs

       style: 'https://demotiles.maplibre.org/style.json',

       center: [0, 0], // starting position [lng, lat]

       zoom: 1 // starting zoom

   });

""", getElement()); // Vaadin makes sure this gets into a reference to the client side counterpart of the Element instance

Recently, I have moved away from this pattern with essentially all but Element references. Simply using the modern JDK’s String.formatted method (introduced in Java 15), you can accomplish pretty much the same, but more flexibly.

Unlike with the core API, one can easily drop in some native JS objects or JSON as parameters. For domain objects I want to share with the JS side, I tend to override toString() methods that produce JS/JSON that can be directly shared with the JS side.

Alternatively, you could use a helper method to serialize as a JS object. I often produce JSON using the Jackson library, but let’s have a trivial example with the coordinate and an array of numbers as its JS type. Here, we now also get a properly typed Java API to define the center of the map:

public record Coordinate(double lon, double lat) {

   @Override
   public String toString() {

       // JS array like toString -> shareable with JS calls
       return "[%s, %s]".formatted(lon, lat);
   }
}

public MapLibreComponent(Coordinate center) {
   loadMapLibreJs();
   setHeight("400px");setWidth("600px");
   getElement().executeJs("""

       var map = new maplibregl.Map({
           container: $0, // parameter via Element.executeJs
           style: 'https://demotiles.maplibre.org/style.json',
           center: %s, // parameter via String.formatted
           zoom: 1
       });

   """.formatted(center), getElement());
}

Although inlining JS into Java files is now with text blocks much cleaner than it used to be, there is a limit to how much JS you want to write this way. Current IDEs don’t highlight your embedded JS snippets in any way, although AI tools like GitHub Co-Pilot seem to do quite a good job mixing Java and JS. Typically, in this kind of integration, the amount of JS is small (the above example is one of the largest), but for larger snippets, I suggest moving the script body to a separate JS file. You can then read the snippets to a string for executeJs method or define helper methods in a separate JS file and then call those using executeJs.

Legibility improvements with templating from JDK 21 or Velocity

When passing in parameters for the JS side, both the built-in parameter passing and the String.formatted approach have an issue with legibility. Parameters you are passing from your Java code are referred to in a non-meaningful way using $0 or %s. The StringTemplate language feature arriving for Java users soon (JEP-430, preview feature in Java 21) will make our lives easier at some point. By passing strings through a “template processor” (most commonly by adding STR. before the string), local variables, methods, and fields can be referenced in the string template. This is how the above code snippet can be written with StringTemplates (tested using the pre-release of JDK 21):

getElement().executeJs(STR."""
   var map = new maplibregl.Map({
       container: $0,
       style: 'https://demotiles.maplibre.org/style.json',
       center: \{center}, // JEP-430 style String interpolation
       zoom: 1
   });
""", getElement());

The pros are obvious. Especially when we also get good IDE support for JEP-430. If you are using the default STR processor, element references are not supported (thus that $0 for getElement() in the example), but creating a custom template processor for Vaadin components that also support element references shouldn't be hard. The con of JDK-provided string templating is that it will still take years until StringTemplate is available for a wide audience. We’ll most likely read a lot of good about it in the upcoming months, but the sad fact is that it is only coming as a preview feature in JDK 21. Wider usage of the feature hopefully starts along with JDK 24 (LTS).

The good parts of JEP-430 inspired me to write a temporary solution with the good old Velocity templating engine. I wrote a simple helper class that add–on developers can use to get somewhat of a similar experience today and still keep their add-ons compatible with other Vaadin users (who most likely can’t use JDK 21 and its preview features). Compared to the upcoming JDK templating, the available parameters need to be separately named, but the Map.of method adding in JDK 9 makes that rather straightforward:

velocityJs("""
   var map = new maplibregl.Map({
       container: ${element}, // velocity template variable
       style: 'https://demotiles.maplibre.org/style.json', // style URL
       center: ${center},
       zoom: 1
   });

   """, Map.of(
               // the variables available for the
               // velocity template are defined here
               "center", center,
               "element", getElement()
           )
);

The nice part of this add-on is that I also managed to bake in support for element references. So, in the above example, you can see we reference both the root element of the component and a DTO providing the center point with a syntax that is both readable and compatible with JS syntax highlighters, in case you separate your Velocity template to a separate JS file.

Disclaimer about the Element API usage

Although most of this post glorifies the powers of the Element API brought to you by the Vaadin Flow framework, and what is basically powering the whole platform, you should remember a couple of things about it:

  • getElement() is for advanced use cases only! It is designed to implement Component classes that you then use in your actual application code. Most of them are in the core libraries or add-ons, but some less generic ones can be in a custom Java class in your application project. But if you see getElement() called anywhere outside your component implementation, you should consider that as a bug. Because of our design flaw, that method is public and too easily discoverable. Thus, the Element abstraction layer too often leaks to actual Vaadin UI code, lowering the quality and maintainability of your code.
  • Pay attention to security! When working with the Element API, you are essentially writing browser code, like you’d work on a client-side framework such as React. The “server-side security” that you usually enjoy with Vaadin is essentially lost when working on this abstraction level. Carefully consider what gets passed to the client side and what gets executed as JS on the client side. And never trust the user input (well, unless you can trust the user in some very rare cases).
  • Mark the potentially insecure API you create! In case you expose an API from your component that passes something “raw” to the client-side, be sure to write that in (even in capitals) to your JavaDocs. These risky parts may create XSS vulnerability to your application if not used accordingly. Also, because half of your users never read the JavaDocs, I encourage you to mark the risky parts to your users by using HTML or JS keywords in the method names, as we do in our core components. Check the chapter about XSS attacks from our documentation, in case you didn’t get this point.

Summary

As a summary of my almost book-length post:

  • Vaadin components don’t have to be backed by a Web Component. Often, it is faster for the component developer to do the integration without one, and there is no difference in the quality of the Java API.
  • JavaScript libraries can be either included in the optimized bundle generated by Vaadin tooling or injected dynamically at runtime. Both have their pros and cons. The trade-off is between developer experience and front-end optimization.
  • The Element API is a powerful way to interact directly with JS libraries (or DOM properties). New and upcoming features in Java are making it even better. Thus, even larger JS integrations can now be built without separate JS files or a Web Component as an abstraction layer.
  • Handle the Element API with care; the strong abstraction layer of Vaadin is not there to back you up.

New to Vaadin? Learn to build and deploy a modern web application completely in Java. Get started ->  

Matti Tahvonen
Matti Tahvonen
Matti Tahvonen has a long history in Vaadin R&D: developing the core framework from the dark ages of pure JS client side to the GWT era and creating number of official and unofficial Vaadin add-ons. His current responsibility is to keep you up to date with latest and greatest Vaadin related technologies. You can follow him on Twitter – @MattiTahvonen
Other posts by Matti Tahvonen