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).
This section demonstrates 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 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 the 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 hasn’t 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 offalse
(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 Binding Data to Forms for more.
You 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’s 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 thegetSource()
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
); -
whether
setValue(null)
should be allowed or throw an exception (false
means thatnull
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 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
|
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.
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 extendsComponentEvent
, which provides a typedgetSource()
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 theevent.
prefix (for exampleevent.offsetX
) and element properties using theelement.
prefix.
Note
|
Constructor parameter requirements
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 theClickEvent
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
|
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 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’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.
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 tells that the user changed the value.
This is because isFromClient()
checks that the change originates from the browser (rather than the server), but doesn’t differentiate between a user changing the value and a change resulting from a JavaScript call.
Note
|
increment() is typically called directly or from server-side logic
The example above is for demonstration purposes only and is somewhat artificial, in that it shows a server visit from a button click 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
|
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.
|
Paper-Slider Integration Result
After you have completed the steps previously described, 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 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 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 = "23.5.8")
@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 (thecreate()
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.
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. Theicon
attribute is set to the correct value (as defined in@vaadin/icons/vaadin-iconset.js
, for exampleicon="vaadin:check"
forVaadinIcon.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.
Example: Importing vaadin-button.js
, vaadin-iconset.js
and vaadin-icon.js
.
@NpmPackage(value = "@vaadin/button", version = "23.5.8")
@JsModule("@vaadin/button")
@NpmPackage(value = "@vaadin/icons", version = "23.5.8")
@JsModule("@vaadin/icons/vaadin-iconset.js")
@NpmPackage(value = "@vaadin/icon", version = "23.5.8")
@JsModule("@vaadin/icon")
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.
|
AACDBA11-3ECD-4E3B-9A36-C64E3963C26C