Creating a Java API for a Web Component

While there are many ways to interact with a Web Component, the most typical are:

  • Use properties on the element to define how it should behave.

  • Listen to events on the element to be notified when the user does something.

  • Call functions on the element to perform specific tasks, for example to open a popup.

  • Add sub-elements to define child content.

The component class included with the Vaadin Add-on Starter, for example PaperSlider.java, is only a stub that handles the imports (see Integrating a Web Component for more). In this section, we demonstrate how to add functionality to the included stub class to transform it into a functioning Java API.

Setting and Reading Properties

You can typically find the properties that an element supports in its JavaScript documentation on npm. See the <paper-slider> documentation, for example. The paper-slider has a boolean property named pin that defines whether or not a numeric-value label displays when the slider thumb is pressed.

You can add getters and setters to match any property to create a corresponding Java setter-getter API for the property.

Example: Adding a setter and getter for the pin property in PaperSlider class.

public void setPin(boolean pin) {
    getElement().setProperty("pin", pin);
}
public boolean isPin() {
    return getElement().getProperty("pin", false);
}
  • The setter sets the given property to the requested value.

  • The getter returns the property value, or false as the default value, if the property has not been set. The default value should match the default of the Web Component property.

When you set the pin to true (by updating the setter in the DemoView class), the pin appears when the slider thumb is pressed.

Example: Calling setPin(true) in the DemoView class.

public DemoView() {
    PaperSlider paperSlider = new PaperSlider();
    paperSlider.setPin(true);
    add(paperSlider);
}

One disadvantage of writing the getElement methods directly, is that you have to repeat the property name in the getter and the setter. You can avoid this by using the PropertyDescriptor helper. This helper, and the factory methods in PropertyDescriptors, allow you to define the property as a single static field in the component and reference it from the getter and the setter.

Example: Using the PropertyDescriptor helper and propertyWithDefault factory method on the pin property.

public class PaperSlider extends AbstractSinglePropertyField<PaperSlider, Integer> {

    private static final PropertyDescriptor<Boolean, Boolean> pinProperty = PropertyDescriptors.propertyWithDefault("pin", false);

    public void setPin(boolean pin) {
        pinProperty.set(this, pin);
    }

    public boolean isPin() {
        return pinProperty.get(this);
    }
}
  • The pinProperty descriptor defines:

    • A property with the name pin and a default value of false (matches the Web Component).

    • Both a setter and getter of type Boolean through generics (<Boolean, Boolean>).

  • The setter and getter code only invokes the descriptor with the component instance.

Synchronizing the Value

The paper-slider component allows the user to input a single value. To make it work automatically as a field, this kind of component should implement the HasValue interface. See in Binding Data to Forms for more.

We need the value to be synchronized automatically from the client to the server when the user changes it, as well as from the server to the client when it is updated programmatically. In addition, a value-change event should be fired on the server whenever the value changes.

In the common use case in which the getValue() method is based on a single-element property, the AbstractSinglePropertyField base class takes care of everything related to the value.

Example: Extending the AbstractSinglePropertyField base class in the PaperSlider class.

public class PaperSlider extends AbstractSinglePropertyField<PaperSlider, Integer> {

    public PaperSlider() {
        super("value", 0, false);
    }

}
  • The type parameters define the component type (PaperSlider) returned by the getSource() method in value change events, and the value type (Integer).

  • The constructor parameters define the name of the element property that contains the value ("value"), the default value to use if the property isn’t set (0), and whether setValue(null) should be allowed or throw an exception (false means that null is not allowed).

For more advanced cases that are still based on a one-element property, there is an alternative constructor that defines callbacks that convert between the low-level element property type and the high-level getValue() type. In cases where the value cannot be derived based on a single-element property, you can use the more generic AbstractField base class. You can test this in the DemoView class.

Example: Testing the use of the alternative constructor in the DemoView class.

public DemoView() {
    PaperSlider paperSlider = new PaperSlider();
    paperSlider.setPin(true);
    paperSlider.addValueChangeListener(e -> {
        String message = "The value is now " + e.getValue();
        if (e.isFromClient()) {
            message += " (set by the user)";
        }
        Notification.show(message, 3000, Position.MIDDLE);
    });
    add(paperSlider);

    Button incrementButton = new Button("Increment using setValue", e -> {
        paperSlider.setValue(paperSlider.getValue() + 5);
    });
    add(incrementButton);
}
Note
For some Web Components you need to update other properties that are not related to the HasValue interface. See Creating A Simple Component Using the Element API on how you can use the @Synchronize annotation to synchronize property values without automatically firing a value-change event.

Listening to Events

All web elements fire a click event when the user clicks them. To allow the user of your component to listen to the click event, you can extend ComponentEvent and use the @DomEvent and @EventData annotations.

Example: Extending ComponentEvent and using the @DomEvent and @EventData annotations in the ClickEvent class.

@DomEvent("click")
public class ClickEvent extends ComponentEvent<PaperSlider> {

    private int x, y;

    public ClickEvent(PaperSlider source,
                      boolean fromClient,
                      @EventData("event.offsetX") int x,
                      @EventData("event.offsetY") int y) {
        super(source, fromClient);
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

}
  • ClickEvent uses the @DomEvent annotation to define the name of the DOM event to listen for (click in this case).

  • Like all other events fired by a Component, it extends ComponentEvent which provides a typed getSource() method.

  • It uses two additional constructor parameters annotated with @EventData to get the click coordinates from the browser.

  • The expression inside each @EventData annotation is evaluated when the event is handled in the browser: It accesses DOM event properties using the event. prefix (for example event.offsetX) and element properties using the element. prefix.

Note
The two first parameters of a ComponentEvent constructor must be PaperSlider source, boolean fromClient and these are filled automatically. All parameters following these two initial parameters must be annotated with @EventData.

You can now use the ClickEvent class as an argument when invoking the addListener method on your PaperSlider component.

Example: Using the ClickEvent class in the addListener method.

public Registration addClickListener(ComponentEventListener<ClickEvent> listener) {
    return addListener(ClickEvent.class, listener);
}
  • The addListener method in the superclass sets up everything related to the event, based on the annotations in the ClickEvent class.

You can test the integration in the DemoView class.

Example: Testing the event integration in the DemoView class.

paperSlider.addClickListener(e -> {
    Notification.show("Clicked at " + e.getX() + "," + e.getY(), 1000, Position.BOTTOM_START);
});
Tip
The click event was used here for illustrative purposes. In a real use case, you should use the ClickEvent provided by Vaadin instead. This also provides additional event details.
Tip
As the event data expression is evaluated as JavaScript, you can control propagation behavior using @EventData("event.preventDefault()") String ignored, for example. This is a workaround when there is no other API to control this behavior.

Calling Element Functions

In addition to properties and events, many elements offer methods that can be invoked for different reasons. For example vaadin-board has a refresh() method that is called whenever a change is made that the Web Component itself is not able to detect automatically. To call a function on an element, you can use the callJsFunction method in Element as a way of providing an API.

Example: Using the callJsFunction method in the PaperSlider class to call the increment function on the paper-slider element.

public void increment() {
    getElement().callJsFunction("increment");
}

You can test this in the DemoView class.

Example: Using incrementJSButton in the DemoView class.

Button incrementJSButton = new Button("Increment using JS", e -> {
    paperSlider.increment();
});
add(incrementJSButton);

If you do this, and also add the value-change listener (described above), you get a notification with the new value after clicking the button. The notification also indicates that the user changed the value. This is because isFromClient checks that the change originates from the browser (as opposed to from the server), but does not differentiate between a user changing the value and a change resulting from a JavaScript call.

Note
The example above is for demonstration purposes only and is somewhat artificial, in that it shows a server visit from a button click in order to call a JavaScript method on another element on client side. In practice, you would either call increment() directly from the client side, or from some other server-side business logic.
Tip
In addition to the method name, callJsFunction accepts an arbitrary number of parameters of supported types. Current supported types are String, Boolean, Integer, Double, and the corresponding primitive types: JsonValue, and Element and Component references. It also returns a server-side promise for the JavaScript function’s return value. See the available methods in the Javadoc for more.

Paper-slider Integration Result

After you have completed the steps described above, your PaperSlider class should be similar to the example below.

Example: Java API provided by the PaperSlider class.

@Tag("paper-slider")
@NpmPackage(value = "@polymer/paper-slider", version = "3.0.1")
@JsModule("@polymer/paper-slider/paper-slider.js")
public class PaperSlider extends AbstractSinglePropertyField<PaperSlider, Integer> {

    private static final PropertyDescriptor<Boolean, Boolean> pinProperty = PropertyDescriptors.propertyWithDefault("pin", false);

    public PaperSlider() {
        super("value", 0, false);
    }

    public void setPin(boolean pin) {
        pinProperty.set(this, pin);
    }

    public boolean isPin() {
        return pinProperty.get(this);
    }

    public Registration addClickListener(ComponentEventListener<ClickEvent> listener) {
        return addListener(ClickEvent.class, listener);
    }

    public void increment() {
        getElement().callJsFunction("increment");
    }
    @DomEvent("click")
    public static class ClickEvent extends ComponentEvent<PaperSlider> {

        private int x, y;

        public ClickEvent(PaperSlider source,
                          boolean fromClient,
                          @EventData("event.offsetX") int x,
                          @EventData("event.offsetY") int y) {
            super(source, fromClient);
            this.x = x;
            this.y = y;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

    }
}

You can extend this class further to support additional configuration properties, for example min and max.

Adding Sub-elements to Define Child Content

Some Web Components can contain child elements. If the component is a layout type and you only want to add child components, implementing the HasComponents interface should be enough. This interface provides default implementations for the add(Component…​), remove(Component…) and removeAll() methods.

Example: Implementing HasComponents to implement your own <div> wrapper.

@Tag(Tag.DIV)
public class Div extends Component implements HasComponents {
}

You can then add and remove components using the provided methods.

Example: Using add methods provided by the HasComponents interface.

Div root = new Div();
root.add(new Span("Hello"));
root.add(new Span("World"));
add(root);

If you do not want to provide a public add/remove API, you have two options: use the Element API or create a new Component to encapsulate the internal element behavior.

As an example, assume you want to create a specialized Vaadin button that can only show a VaadinIcon.

Example: Using the available VaadinIcon enum (that lists the icons in the set).

@Tag("vaadin-button")
@NpmPackage(value = "@vaadin/button", version = "22.0.0-alpha10")
@JsModule("@vaadin/button/vaadin-button.js")
public class IconButton extends Component {

    private VaadinIcon icon;

    public IconButton(VaadinIcon icon) {
        setIcon(icon);
    }

    public void setIcon(VaadinIcon icon) {
        this.icon = icon;

        Component iconComponent = icon.create();
        getElement().removeAllChildren();
        getElement().appendChild(iconComponent.getElement());
    }

    public void addClickListener(
            ComponentEventListener<ClickEvent<IconButton>> listener) {
        addListener(ClickEvent.class, (ComponentEventListener) listener);
    }

    public VaadinIcon getIcon() {
        return icon;
    }
}
  • The relevant part here is in the setIcon method. VaadinIcon happens to include a feature that creates a component for a given icon (the create() call) that is used here to create the child element.

  • After creating the element, all that is necessary is to attach the root element of the child component by calling getElement().appendChild(iconComponent.getElement());.

If the VaadinIcon.create() method was not available, you would need to either create the component yourself or use the Element API directly.

Example: Using the Element API to define the setIcon method.

public void setIcon(VaadinIcon icon) {
    this.icon = icon;
    getElement().removeAllChildren();

    Element iconElement = new Element("vaadin-icon");
    iconElement.setAttribute("icon", "vaadin:" + icon.name().toLowerCase().replace("_", "-"));
    getElement().appendChild(iconElement);
}
  • The first part is the same as the previous example. However, in the second part, the element with the correct tag name, <vaadin-icon>, is created manually and the icon attribute is set to the correct value (as defined in @vaadin/vaadin-icons/vaadin-iconset.js, for example icon="vaadin:check" for VaadinIcon.CHECK).

  • After creation, the element is attached to the <vaadin-button> element, after removing any previous content.

When using the second approach, you must also ensure that the vaadin-button.js dependency is loaded, otherwise it is handled by the Icon component class.

Example: Importing vaadin-button.js, vaadin-iconset.js and vaadin-icon.js.

@NpmPackage(value = "@vaadin/vaadin-button", version = "22.0.0-alpha10")
@JsModule("@vaadin/vaadin-button/vaadin-button.js")
@NpmPackage(value = "@vaadin/vaadin-icons", version = "22.0.0-alpha10")
@JsModule("@vaadin/vaadin-icons/vaadin-iconset.js")
@NpmPackage(value = "@vaadin/vaadin-icon", version = "22.0.0-alpha10")
@JsModule("@vaadin/vaadin-icon/vaadin-icon.js")
public class IconButton extends Component {

You can test either approach in the DemoView class.

Example: Testing the icon button sub-element in the DemoView class.

IconButton iconButton = new IconButton(VaadinIcon.CHECK);
iconButton.addClickListener(e -> {
    int next = (iconButton.getIcon().ordinal() + 1) % VaadinIcon.values().length;
    iconButton.setIcon(VaadinIcon.values()[next]);
});
add(iconButton);
  • This shows the CHECK icon and then changes the icon on every click of the button.

Note
You could extend Button directly (instead of Component), but you would then also inherit the entire public Button API.