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:
Java
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
Java
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:
Java
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.
Java
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:
Java
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:
Java
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:
Java
@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
:
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
:
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
:
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:
Java
@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
Java
@Tag(Tag.DIV)
public class Div extends Component implements HasComponents {
}
You can then add and remove components using the provided methods, e.g.
Java
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
Java
@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:
Java
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:
Java
@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.
Java
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 |