Creating Java API for a Web Component

The component class, e.g. PaperSlider.java you get when using the component starter, see Integrating a Web Component is just a stub which handles the imports. There are multiple ways to interact with a web component but the typical pattern is:

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

  • Listen to events on the element to get notified of when the user does something

  • Call functions on the element to perform specific tasks such as open a popup

  • Add sub elements to define child contents

Setting and reading properties

You can typically find out what properties an element supports from its JS docs, e.g. for paper-slider: https://www.webcomponents.org/element/PolymerElements/paper-slider/elements/paper-slider. The slider has a boolean property called pin which defines if "numeric value label is shown when the slider thumb is pressed". To create a setPin(boolean pin) Java API for this, you can add:

public void setPin(boolean pin) {
    getElement().setProperty("pin", pin);
}
public boolean isPin() {
    return getElement().getProperty("pin", false);
}

The setter will now set the given property to the requested value and the getter will return the property value, or false as the default if the property has not been set (this should match the default of the web component property).

If you then update DemoView

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

you will see the pin appear when dragging the slider knob.

While you can use the getElement methods directly like above, you will end up in defining the property in two places: the getter and the setter. To avoid repeating the property name and to be able to define all properties in one place, there is a PropertyDescriptor helper. Using PropertyDescriptor and the factory methods in PropertyDescriptors, you can define the pin property as a static field in the component and use the PropertyDescriptor in the getter and the setter:

public class PaperSlider extends Component {

    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 here defines a property with the name pin and a default value of false (matches the web component) and both a setter and getter type of Boolean through generics (<Boolean, Boolean>). The setter and getter code then only invokes the descriptor with the component instance.

Synchronizing the Value

paper-slider is an input type component where the user can decide the what the value is. These kind of components should be integrated using the HasValue interface so they can automatically work in forms where you use Binder.

The value should be synchronized automatically from the client to the server, when the user changes it, and from the server to the client, when changing it form code. Additionally a value change event should be emitted on the server whenever the value changes. For the most common case where getValue() is based on a single element property, the AbstractSinglePropertyField base class can be used to take care of everything related to the value.

public class PaperSlider extends AbstractSinglePropertyField<PaperSlider, Integer> {

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

}

The type parameters define the component type (PaperSlider) returned by getSource() 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 there property isn’t set (0) and whether setValue(null) should be allowed or throw an exception (false, meaning that null is not allowed).

Note
For more advanced cases that still rely on only one element property, there’s an alternative constructor for defining callbacks that convert between the low-level element property type and the high level getValue() type. For cases where the value cannot be derived based on a single element property, there’s a more generic AbstractField base class.

You can test this for instance as follows in the demo 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
Some web components also update other properties that are not related to HasValue. Creating A Simple Component Using the Element API describes how you can use the @Synchronize annotation to synchronize property values without automatically firing a value change event.

Listening to Events

All web elements emit a click event when the user click on them. To allow the user of your component to listen to the click event, you can use a ComponentEvent together with the @DomEvent and @EventData annnotations:

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

The addListener method in the super class will set up everything related to the event based on the annotations in the ClickEvent class that also need to be created:

@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;
    }
    public int getX() {
        return x;
    }
    public int getY() {
        return y;
    }
}

The ClickEvent uses @DomEvent 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.

The click event defined above uses two additional constructor parameter annotated with @EventData to get the click coordinates from the browser. The expression inside the @EventData is evaluated when the event is handled in the browser and can access DOM event properties using a event. prefix and element properties using the element. prefix, e.g. event.offsetX.

Finally, you can test the event integration in the demo e.g. by adding to DemoView.java:

paperSlider.addClickListener(e -> {
    Notification.show("Clicked at " + e.getX() + "," + e.getY(), 1000, Position.BOTTOM_START);
});
Note
The two first parameters to a ComponentEvent constructor must be PaperSlider source, boolean fromClient which are filled automatically. Any @EventData parameters must be added after those and all additional parameters must have an @EventData annotation.
Tip
The click event here is used as an example. You should use the ClickEvent already provided in Flow instead, which will provide even more information to the server.
Tip
As the event data expression is evaluated as JavaScript, you can control propagation behavior using e.g. @EventData("event.preventDefault()") String ignored. Don’t do this. It ain’t right. But as long as there is no other API to control this, you can do this.

Calling Element Functions

In addition to properties and events, many elements offer methods which can be invoked for various reasons, e.g. vaadin-board has a refresh() method which 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 callFunction method in Element, e.g. to offer an API to the increment function on paper-slider, you could add to PaperSlider.java:

public void increment() {
    getElement().callFunction("increment");
}
Tip
In addition to the method name, callFunction takes an arbitrary number of parameters of certain supported types. Supported types are at the time of writing String, Boolean, Integer, Double, the corresponding primitive types, JsonValue, Element and Component references. See the method javadoc for more information about supported types.

You can test this by adding a call to DemoView.java:

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

If you do this and added the value change listener described earlier, you will see that you get a notification with the new value after clicking on the button. The notification also indicates that the user changed the value because isFromClient checks that the change originates from the browser (as opposed to from the server) but does not differentiate between the cases when a user event changed the value and when a JavaScript call changed it.

Note
This particular example is quite artificial as you are doing a server visit from a button click only to call a Javascript method on another element. It makes more sense if you call increment() from some other business logic.

Final Slider Integration Result

After doing the steps described above, you should end up with the following PaperSlider class:

@Tag("paper-slider")
@HtmlImport("bower_components/paper-slider/paper-slider.html")
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().callFunction("increment");
    }
}

This can now be further extended to support more configuration properties like min and max.

Add Sub Elements to Define Child Contents

Some web components can contain child elements. If the component is a layout type where you just want to add child components, it is enough to implement HasComponents. The HasComponents interface provides default implementations for add(Component…​), remove(Component…) and removeAll(). As an example, you could implement your own <div> wrapper as

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

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

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 for encapsulating the internal element behavior.

As an example, say you wanted to create a specialized Vaadin Button which can only show a VaadinIcon. Using the available VaadinIcon enum, which lists the icons in the set, you can do e.g

@Tag("vaadin-button")
@HtmlImport("bower_components/vaadin-button/vaadin-button.html")
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. As there happens to be a feature in VaadinIcon which creates a component for a given icon (the create() call), it is used to create the child element. What remains is then to attach the root element of the child component, done using getElement().appendChild(iconComponent.getElement());.

In case the VaadinIcon.create() method was not available, you would have to resort to either creating the component yourself or using the element API directly. If you use the element API, the setIcon method might look something like:

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

    Element iconElement = new Element("iron-icon");
    iconElement.setAttribute("icon", "vaadin:" + icon.name().toLowerCase().replace("_", "-"));
    getElement().appendChild(iconElement);
}

The first part is the same but in the second part, the element with the correct tag name <iron-icon> is created manually and the icon attribute is set to the correct value, defined in vaadin-icons.html, e.g. icon="vaadin:check" for VaadinIcon.CHECK. The element is then attached to the <vaadin-button> element, after removing any previous content. With this approach you must also ensure that the vaadin-button.html dependency is loaded, otherwise handled by the Icon component class:

@HtmlImport("bower_components/vaadin-button/vaadin-button.html")
@HtmlImport("bower_components/vaadin-icons/vaadin-icons.html")
public class IconButton extends Component {

Regardless of the approach taken, you can test the icon button as e.g.

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 will show the CHECK icon and then change the icon on every click of the button.

Note
You could extend Button directly instead of Component but then you would get all the public API of Button also