Creating a Component that Has a Value
- Helper Classes
- Using a Single-element Property as the Value
- Combining Properties into One Value
- Creating Fields from Other Fields
The basic property of a field component is that it has a value, which the user can edit. To express that, a field must implement the HasValue
interface. This is a requirement in order for a component to work with Binder
.
The HasValue
interface defines:
-
methods to access the value itself;
-
an event when the value changes;
-
helpers to deal with empty values;
-
a
ReadOnly
mode; -
a
required
indicator.
Helper Classes
You can use the following classes as a base class for custom fields:
-
AbstractField
is the most basic, but also the most flexible base class. It has many details that you need to take care of, but it supports complex use cases. -
AbstractCompositeField
is similar toAbstractField
, except that it usesComposite
instead ofComponent
as the base class. It’s suitable when the value input component consists of several individual components. -
AbstractSinglePropertyField
is suitable when the value is the property of the single element of the component. This base class simplifies a common use case found in many Web Components that are similar in design to the native<input>
element.
Using a Single-element Property as the Value
Many components extend Web Components that have a property that contains the component’s value.
The property name is typically value
, and it fires a value-changed
event when changed.
When the property type is a string, number, or Boolean, all you need to do is to extend AbstractSinglePropertyField
and call its constructor with the name of the property, the default value, and whether it allows null values.
The paper-slider
Web Component is a compliant example.
It has an integer property named value
, it displays the slider at the 0 position if the value isn’t set, and doesn’t support showing no value at all.
For example, consider a PaperSlider
component that extends AbstractSinglePropertyField
:
@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> {
public PaperSlider() {
super("value", 0, false);
}
}
-
The type parameters of
AbstractSinglePropertyField
are:-
The type of the
getSource()
method in fired value-change events (PaperSlider
). -
The value type (
Integer
).
-
-
The default value of
0
is automatically used by theclear()
andisEmpty()
methods.clear()
sets the field value to the default value, andisEmpty()
returnstrue
if the field value is the default value.
Note
|
Vaadin Uses Polymer 3
Vaadin uses Polymer 3.
This version provides the best compatibility for integrating third-party Web Components.
|
Converting Property Values
With some Web Components, there is a Java type that’s more suitable than the type of the element property.
It’s possible to configure AbstractSinglePropertyField
to apply a converter when changing, reading, or writing the value to the element property.
For example, the value
property of <input type="date">
is an ISO 8601-formatted string (YYYY-MM-DD
). You can convert this into a DatePicker
component to select a LocalDate
.
Example: DatePicker
component that allows the selection of a LocalDate
.
It extends AbstractSinglePropertyField
and provides a callback to convert from LocalDate
to String
, and a callback in the opposite direction.
@Tag("input")
public class DatePicker
extends AbstractSinglePropertyField<DatePicker,
LocalDate> {
public DatePicker() {
super("value", null, String.class,
LocalDate::parse,
LocalDate::toString);
getElement().setAttribute("type", "date");
setSynchronizedEvent("change");
}
@Override
protected boolean hasValidValue() {
return isValidDateString(getElement()
.getProperty("value"));
}
}
In this scenario, the convention of listening for an event named <propertyName>-changed
is inappropriate.
Instead, the setSynchronizedEvent("change")
call overrides the default configuration, and listens for the change event in the browser.
Overriding the hasValidValue()
method validates the element value before it gets passed to the LocalDate.parse()
method that’s defined in the constructor.
In this way, invalid values are ignored, instead of causing exceptions.
Combining Properties into One Value
AbstractSinglePropertyField
works with Web Components that have the value in a single-element property.
In other cases, the value of a component can be a composition of properties in one or more elements.
In such cases, the best solution is often to extend AbstractField
.
When you extend AbstractField
, there are two different value representations to handle:
- Presentation value
-
The value displayed to the user in the browser, for example as element properties.
- Model value
-
The value available through the
getValue()
method.
The field needs to keep the two values in sync, unless:
-
the value is being changed, or
-
the element properties are in an invalid state that can’t, or shouldn’t, be represented through
getValue()
.
For example, consider a simple-date-picker
Web Component that has separate integer properties for the selected date: year
, month
and dayOfMonth
.
For each property, there is a corresponding event when the user makes a change: year-changed
, month-changed
, and day-of-month-changed
.
Start by implementing a SimpleDatePicker
component that extends AbstractField
and passes the default value to its constructor.
@Tag("simple-date-picker")
public class SimpleDatePicker
extends AbstractField<SimpleDatePicker, LocalDate> {
public SimpleDatePicker() {
super(null);
}
}
Note
|
The type parameters are the same as for AbstractSinglePropertyField : the getSource() type for the value-change event and the value type.
|
When you call setValue(T value)
with a new value, AbstractField
invokes the setPresentationValue(T value)
method with the new value.
You need to implement the setPresentationValue(T value)
method so that the component updates the element properties to match the values set:
@Override
protected void setPresentationValue(LocalDate value) {
Element element = getElement();
if (value == null) {
element.removeProperty("year");
element.removeProperty("month");
element.removeProperty("dayOfMonth");
} else {
element.setProperty("year", value.getYear());
element.setProperty("month",
value.getMonthValue());
element.setProperty("dayOfMonth",
value.getDayOfMonth());
}
}
To handle value changes coming from the browser, the component must listen to appropriate internal events and pass a new value to the setModelValue(T value, boolean fromClient)
method.
AbstractField
uses this to check whether the provided value has actually changed and, if it has, it fires a value-change event to all listeners.
You need to update the constructor to define each of the element properties as synchronized, and add the same property-change listener to each of them:
public SimpleDatePicker() {
super(null);
setupProperty("year", "year-changed");
setupProperty("month", "month-changed");
setupProperty("dayOfMonth", "dayOfMonth-changed");
}
private void setupProperty(String name, String event) {
Element element = getElement();
element.addPropertyChangeListener(name, event,
this::propertyUpdated);
}
Tip
|
By default, AbstractField calls Objects.equals() to determine whether a new value is the same as the previous value.
If the equals() method of the value type isn’t appropriate, you can override the valueEquals() method to implement your own comparison logic.
|
Warning
|
Use AbstractField only with immutable-value instances
AbstractField should only be used with immutable-value instances.
No value-change event is fired if the original getValue() instance is modified and passed to setModelValue() or setValue() .
|
The final step is to implement the property-change listener to create a new LocalDate
based on the element property values, and pass it to setModelValue()
.
private void propertyUpdated(
PropertyChangeEvent event) {
Element element = getElement();
int year = element.getProperty("year", -1);
int month = element.getProperty("month", -1);
int dayOfMonth = element.getProperty(
"dayOfMonth", -1);
if (year != -1 && month != -1 && dayOfMonth != -1) {
LocalDate value = LocalDate.of(
year, month, dayOfMonth);
setModelValue(value, event.isUserOriginated());
}
}
If any of the properties aren’t filled in, setModelValue()
isn’t called.
This means that getValue()
returns the same value it returned the previous time it was called.
The component can call setModelValue()
from inside its setPresentationValue()
implementation.
In this case, the call sets the value of the component to the value passed to setModelValue()
.
This value is used instead of the original value.
This is useful to transform provided values, for example to make all strings uppercase.
If you have a percentage field whose value can only be in the range 0 to 100, for example, you can use:
@Override
protected void setPresentationValue(Integer value) {
if (value < 0) value = 0;
if (value > 100) value = 100;
getElement().setProperty("value", false);
}
If the value set from the server is, for example, 138, the following code sets the value to 100 on the client, but the internal server value remains 138:
@Override
protected void setPresentationValue(Integer value) {
if (value < 0) value = 0;
if (value > 100) value = 100;
getElement().setProperty("value", value);
setModelValue(value, false);
}
Calling setModelValue()
from the setPresentationValue()
implementation doesn’t fire a value-change event.
If setModelValue()
is called multiple times, the value of the last invocation is used, and there is no need to worry about causing infinite loops.
Creating Fields from Other Fields
AbstractCompositeField
makes it possible to create a field component that has a value based on the value of one or more internal fields.
For example, consider an employee selector field that allows the user first to select a department from a combo box, and then to select an employee from that department in another combo box.
The component itself is a Composite
, based on a HorizontalLayout
that contains the two ComboBox
components, displayed side by side.
Tip
|
Another use case for AbstractCompositeField is to create a field component that’s based directly on another field, while converting the value from that field.
|
The class declaration is a mix of Composite
and AbstractField
.
-
The first type parameter defines the
Composite
content type, the second is for the value-change eventgetSource()
type, and the third is thegetValue()
type of the field. -
Initialize instance fields for each
ComboBox
.
public class EmployeeField extends
AbstractCompositeField<HorizontalLayout,
EmployeeField, Employee> {
private ComboBox<Department> departmentSelect =
new ComboBox<>("Department");
private ComboBox<Employee> employeeSelect =
new ComboBox<>("Employee");
}
In the constructor:
-
Configure
departmentSelect()
value changes to update the items inemployeeSelect()
. -
The constructor sets the employee selected in
employeeSelect()
as the field’s value. -
Both combo boxes are added to the horizontal layout.
public EmployeeField() {
super(null);
departmentSelect.setItems(
EmployeeService.getDepartments());
departmentSelect.addValueChangeListener(event -> {
Department department = event.getValue();
employeeSelect.setItems(EmployeeService
.getEmployees(department));
employeeSelect.setEnabled(department != null);
});
employeeSelect.addValueChangeListener(event ->
setModelValue(event.getValue(), true));
getContent().add(departmentSelect, employeeSelect);
}
As a next step, implement setPresentationValue()
to update the combo boxes according to a specified employee.
@Override
protected void setPresentationValue(Employee employee) {
if (employee == null) {
departmentSelect.clear();
} else {
departmentSelect.setValue(
employee.getDepartment());
employeeSelect.setValue(employee);
}
}
You can change how the required indicator is displayed for the field.
The implementation that follows assumes that the component’s root element reacts to a property with name required
, which works well with Web Components that mimic the API of the <input>
element.
In this example, the required indicator is displayed for the employee combo box.
@Override
public void setRequiredIndicatorVisible(
boolean required) {
employeeSelect.setRequiredIndicatorVisible(required);
}
@Override
public boolean isRequiredIndicatorVisible() {
return employeeSelect.isRequiredIndicatorVisible();
}
You need to handle the readonly
property by marking both combo boxes as read-only.
The following implementation is similar to how required
indicators are handled in the previous example, except that it uses the readonly
property.
@Override
public void setReadOnly(boolean readOnly) {
departmentSelect.setReadOnly(readOnly);
employeeSelect.setReadOnly(readOnly);
}
@Override
public boolean isReadOnly() {
return employeeSelect.isReadOnly();
}
4F463F71-4C2A-480C-A507-AC5D8F39B46D