Creating a Java API for a Web Component
While there are many ways to interact with a Web Component, there are a few typical ones worth noting. You could use properties on the element to define how it should behave. You can listen to events on the element to be notified when the user does something. Or you could call functions on the element to perform specific tasks, such as open a popup. And you can add sub-elements to define child content.
The component class included with the Vaadin Add-on Starter, for example MwcSlider.java
, is only a stub that handles the imports (see Integrating a Web Component for more). This page demonstrates how to add functionality to the included stub class to transform it into a functioning Java API.
Setting & Reading Properties
You can typically find the properties that an element supports in its JavaScript documentation on npm
(see the <mwc-slider>
documentation). The mwc-slider
has a boolean property named discrete
that defines whether 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. An example of this would be adding a setter and getter for the discrete
property in the MwcSlider
class, like so:
public void setDiscrete(boolean discrete) {
getElement().setProperty("discrete", discrete);
}
public boolean isDiscrete() {
return getElement().getProperty("discrete", false);
}
The setter here sets the given property to the requested value. The getter returns the property value, or false
as the default value, if the property hasn’t been set. The default value should match the default of the Web Component property.
When you set discrete
to true
— which is done by updating the setter in the AddonView
class — the pin appears when the slider thumb is pressed.
Calling setDiscrete(true)
in the AddonView
class, for example, would look like this:
public AddonView() {
MwcSlider slider = new MwcSlider();
slider.setDiscrete(true);
add(slider);
}
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
, allows you to define the property as a single static field in the component and reference it from the getter and the setter.
For example, using the PropertyDescriptor
helper and propertyWithDefault()
factory method on the discrete
property might be done like so:
public class MwcSlider extends Component {
private static final PropertyDescriptor<Boolean, Boolean> discreteProperty =
PropertyDescriptors.propertyWithDefault("discrete", false);
public void setDiscrete(boolean discrete) {
discreteProperty.set(this, discrete);
}
public boolean isDiscrete() {
return discreteProperty.get(this);
}
}
The discreteProperty
descriptor here defines a property with the name discrete
and a default value of false
, which matches the Web Component. Also, both a setter and getter of type Boolean
are set through generics (<Boolean, Boolean>
). The setter and getter code, though, only invokes the descriptor with the component instance.
Synchronizing the Value
The mwc-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 Binding Data to Forms for more information on this.
The value needs 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’s updated programmatically. Additionally, a value-change event should be fired on the server whenever the value changes.
Typically, when the getValue()
method is based on a single-element property, the AbstractSinglePropertyField
base class takes care of everything related to the value.
As an example, the following would extend the AbstractSinglePropertyField
base class in the MwcSlider
class:
public class MwcSlider extends AbstractSinglePropertyField<MwcSlider, Integer> {
public MwcSlider() {
super("value", 0, false);
}
}
The type parameters here define the component type (MwcSlider
) returned by the getSource()
method in value change events, and the value type (Integer
). The constructor parameters in this example 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 it determines whether setValue(null)
should be allowed or throw an exception (i.e., false
means that null
isn’t 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 can’t be derived based on a single-element property, you can use the more generic AbstractField
base class.
You can test this in the AddonView
class. For example, testing the use of the alternative constructor in the AddonView
class might look like this:
public AddonView() {
MwcSlider slider = new MwcSlider();
slider.setDiscrete(true);
slider.addValueChangeListener(e -> {
String message = "The value is now " + e.getValue();
if (e.isFromClient()) {
message += " (set by the user)";
}
Notification.show(message, 3000, Notification.Position.MIDDLE);
});
add(slider);
Button incrementButton = new Button("Increment using setValue", e -> {
slider.setValue(slider.getValue() + 5);
});
add(incrementButton);
}
Note
|
Properties not related to the HasValue interface
For some Web Components, you need to update other properties that aren’t related to the HasValue interface. See Creating A Simple Component Using the Element API for how to 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. For example, to extend ComponentEvent
and use the @DomEvent
and @EventData
annotations in the ClickEvent
class, you might do something like this:
@DomEvent("click")
public static class ClickEvent extends ComponentEvent<MwcSlider> {
private int x, y;
public ClickEvent(MwcSlider 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
here uses the @DomEvent
annotation to define the name of the DOM event for which to listen: 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 (e.g., event.offsetX
) and element properties using the element.
prefix.
Note
|
Constructor parameter requirements
The two first parameters of a ComponentEvent constructor must be MwcSlider source, boolean fromClient . 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 MwcSlider
component. The example here uses the ClickEvent
class in the addListener()
method:
public Registration addClickListener(ComponentEventListener<ClickEvent> listener) {
return addListener(ClickEvent.class, listener);
}
The addListener()
method shown here 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 AddonView
class. To test the event integration in the AddonView
class, you would do something like this:
slider.addClickListener(e -> {
Notification.show("Clicked at " + e.getX() + "," + e.getY(), 1000, Notification.Position.BOTTOM_START);
});
Tip
|
Use the Vaadin-provided ClickEvent for production
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
|
Controlling propagation behavior
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 work-around 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’s called whenever a change is made that the Web Component itself isn’t 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.
Using the callJsFunction()
method, for example, in the MwcSlider
class to call the layout
function on the mwc-slider
element would be like this:
public void layout(boolean skipUpdateUI) {
getElement().callJsFunction("layout", skipUpdateUI);
}
You can test this in the AddonView
class, for example, by using layoutJSButton
in the AddonView
class.
Button layoutJSButton = new Button("Layout component using JS", e -> {
slider.layout(false);
});
add(layoutJSButton);
The above method recomputes the dimensions and lays out again the component. This should be called if the dimensions of the slider itself, or any of its parent elements, change programmatically. To check the results manually, set width of <mwc-slider>
to a fixed value and then click the button.
Tip
|
callJsFunction() parameters and return value
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.
|
Mwc-Slider Integration Result
After you have completed the steps previously described, your MwcSlider
class should be similar to the example below. In this example, Java API is provided by the MwcSlider
class:
@Tag("mwc-slider")
@NpmPackage(value = "@material/mwc-slider", version = "0.27.0")
@JsModule("@material/mwc-slider/slider.js")
public class MwcSlider extends AbstractSinglePropertyField<MwcSlider, Integer> {
private static final PropertyDescriptor<Boolean, Boolean> discreteProperty = PropertyDescriptors
.propertyWithDefault("discrete", false);
public MwcSlider() {
super("value", 0, false);
}
@Synchronize("change") // synchronize value onChange event
@Override
public Integer getValue() {
return super.getValue();
}
public Registration addClickListener(
ComponentEventListener<ClickEvent> listener) {
return addListener(ClickEvent.class, listener);
}
public void setDiscrete(boolean discrete) {
discreteProperty.set(this, discrete);
}
public boolean isDiscrete() {
return discreteProperty.get(this);
}
public void layout(boolean skipUpdateUI) {
getElement().callJsFunction("layout", skipUpdateUI);
}
@DomEvent("click")
public static class ClickEvent extends ComponentEvent<MwcSlider> {
private int x, y;
public ClickEvent(MwcSlider 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, such as min
and max
.
Adding Sub-Elements to Define Child Content
Some Web Components can contain child elements. If a 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.
For example, implementing HasComponents
to implement your own <div>
wrapper would be done like so:
@Tag(Tag.DIV)
public class Div extends Component implements HasComponents {
}
You can then add and remove components using the provided methods. For example, using add()
methods provided by the HasComponents
interface like so:
Div root = new Div();
root.add(new Span("Hello"));
root.add(new Span("World"));
add(root);
If you don’t 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 that you want to create a specialized Vaadin button that can only show a VaadinIcon
. Below shows how to do that using the available VaadinIcon
enum that lists the icons in the set:
@Tag("vaadin-button")
@NpmPackage(value = "@vaadin/button", version = "24.6.0-alpha7")
@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’s used here to create the child element. After creating the element, all that’s necessary is to attach the root element of the child component by calling getElement().appendChild(iconComponent.getElement())
.
If the VaadinIcon.create()
method wasn’t available, you would need to either create the component yourself or use the Element
API directly. Using the Element
API, for example, to define the setIcon()
method would look like this:
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 here is the same as the previous example. However, in the second part, the element with the correct tag name, <vaadin-icon>
, is created manually. The icon
attribute is set to the correct value (as defined in @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’s handled by the Icon
component class. An example of this would be importing vaadin-button.js
, vaadin-iconset.js
and vaadin-icon.js
like so:
@NpmPackage(value = "@vaadin/button", version = "24.6.0-alpha7")
@JsModule("@vaadin/button")
@NpmPackage(value = "@vaadin/icons", version = "24.6.0-alpha7")
@JsModule("@vaadin/icons/vaadin-iconset.js")
@NpmPackage(value = "@vaadin/icon", version = "24.6.0-alpha7")
@JsModule("@vaadin/icon")
public class IconButton extends Component {
You can test either approach in the AddonView
class. For example, to test the icon button sub-element in the AddonView
class, you would do something like this:
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.
|
AACDBA11-3ECD-4E3B-9A36-C64E3963C26C