Seiten

Sonntag, 13. Oktober 2013

Using JavaScript libraries (D3) in Vaadin webapplications

This is a short tutorial on how to use JavaScript libraries with Vaadin.

The library that shall be used is D3, which is a powerful library for creating data-driven diagrams using JavaScript.

To keep it simple the final UI will look like this (see the screenshot at the end of the posting): We have a coordinate system in which we have a circle which can be moved inside that coordinate system. The coordinates can be entered in textfields and applied by clicking a button. Maybe there will be another post with some more meaningful use of D3 inside a Vaadin-Application in the future. But this tutorial aims on how to use JavaScript libraries inside Vaadin applications in general.. so let's start!

We have to create two Java classes: Diagram, DiagramState
And we have to create one JavaScript file: diagram_connector.js

First, let's have a look at the Java classes "Diagram" and "DiagramState". The former extends "AbstractJavaScriptComponent". It is the component that is added to the UI in the end (like a Button or a TextField). The latter extends JavaScriptComponentState and is just needed for the communication between Vaadin and JavaScript (or server and client). Here is the code:

Diagram
@JavaScript({"d3.v3.min.js",
        "diagram_connector.js"})
public class Diagram extends AbstractJavaScriptComponent {

    public void setCoords(final List<Integer> coords) {
        getState().setCoords(coords);
    }

    @Override
    public DiagramState getState() {
        return (DiagramState) super.getState();
    }
}
The @JavaScript annotation lists all JavaScrip files that have to be used. The first one is the D3 Library and the second one is the file that we have to create later. In my case both files are in the same folder in which the Vaadin UI-extending class is (AND I had to copy the files into my war manually, but that's a configuration issue of IntelliJ..).

The setCoords method is used to give the Diagram component a list with the x- and y-coordinates of the position the circle should be moved to. The list is delegated to a corresponding attribute of the DiagramState.

The overriden method getState looks always the same (with exception of the name of the State class, of course).

DiagramState
public class DiagramState extends JavaScriptComponentState {

    private List<Integer> coords;

    public List<Integer> getCoords() {
        return coords;
    }

    public void setCoords(final List<Integer> coords) {
        this.coords = coords;
    }
}
The DiagramState class has an attribute which is a list of integers. It contains the x- and y-coordinates of the position the circle should be moved to (inside the coordinate system) as explained above. The rest is simple getters and setters for that attribute.

Now let's have a look at the JavaScript file we have to create.

diagram_connector.js

window.org_rapidpm_vaadinwithjs_Diagram = function() {
    var diagramElement = this.getElement();
    var diagramFrame = d3.select(diagramElement).append("svg:svg").attr("width", 500).attr("height", 500);
    diagramFrame.append("svg:circle").attr("cx", 250).attr("cy", 250).attr("r", 20).attr("fill", "red");

    this.onStateChange = function() {
        var coords = this.getState().coords;
        d3.selectAll("circle").transition().attr("cx", parseInt(coords[0]));
        d3.selectAll("circle").transition().delay(500).attr("cy", parseInt(coords[1]));
    }
}
The first function assigment must always be done that way. Recognize that you assign a function to window.<fully qualified name of the class which extends AbstractJavaScriptComponent with underscores replacing the dots>.

this.getElement() returns the corresponding DOM element for our diagram. (Think of document.getElementById(<the ID of the Diagram element>)).

The following two lines are D3 related. We "select" our element and append a coordinate system to it with a width and height of 500. Then we insert a circle into that coordinate system at coordinates 250,250 (which is the center), a radius of 20 and a red fill color. For more information on how to use the D3 library in detail have a look at their website. There are some nice tutorials.

The assignment of a function to this.onStateChange is always called when the state of the Diagram changes. This happens if we call the setCoords()-method of our Diagram class. If that is the case we get the coords from the Diagram's state. Recognize that coords (which is an Integer-List in the corresponding Java class) is a number array for JavaScript now. As next we select all circles (right, we only have one in our case). Then we move the circle to the new x- and y-coordinate (x = coords[0], y=coords[1]) via a transition (which simply looks cooler).

That's all the magic. Now at last let's have a look at the main class with the init-method of our UI. We have to TextFields for x and y coordinate, a Button to apply the new coordinates and, of course, our Diagram which we can use as any other component as said above:

VaadinWithJavaScriptComponentsUI

public class VaadinWithJavaScriptComponentsUI extends UI {
    final VerticalLayout layout = new VerticalLayout();   
    final TextField xCoordField = new TextField("X");
    final TextField yCoordField = new TextField("Y");
    final Button button = new Button("move circle");
    final Diagram diagram = new Diagram();
    final List<Integer> coords = new ArrayList<>();

    @Override
    protected void init(VaadinRequest request) {

        configureIntegerField(xCoordField);     //not interesting, just adding converter/validator to the textFields
        configureIntegerField(yCoordField);

        button.addClickListener(new Button.ClickListener() {   //ATTENTION! Here we get the coordinates from the textfields and apply them to our Diagram via calling diagram.setCoords()
            @Override
            public void buttonClick(Button.ClickEvent event) {
                if(xCoordField.isValid() && yCoordField.isValid()){
                    coords.clear();
                    coords.add(Integer.parseInt(xCoordField.getValue()));
                    coords.add(Integer.parseInt(yCoordField.getValue()));
                    diagram.setCoords(coords);
                }
            }
        })
        //now we build the layout.
        layout.setSpacing(true);
        layout.addComponent(xCoordField);
        layout.addComponent(yCoordField);
        layout.addComponent(button);
        layout.addComponent(diagram);     //add the diagram like any other vaadin component, cool!
        setContent(layout);
    }

    private void configureIntegerField(final TextField integerField) {
        integerField.setConverter(Integer.class);
        integerField.addValidator(new IntegerRangeValidator("only integer, 0-500", 0, 500));
        integerField.setRequired(true);
        integerField.setImmediate(true);
    }
}

That's it. Here is a screenshot of the result and some (kind of unspectacular) animated pictures.



sources: