Creating a Component that Has a Value

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. Binder requires that for a component to work with it.

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 to AbstractField, except that it uses Composite instead of Component as the base class. It is suitable when the value input component is 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 is not set, and does not 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 the clear() and isEmpty() methods: clear() sets the field value to the default value, and isEmpty() returns true if the field value is the default value.

Note
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 is more suitable than the type of the element property.

It is 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 for selecting 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 is 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 case, 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 both values sync, unless the value is being changed, or the element properties are in an invalid state that cannot, or should not, 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 from the user’s 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 if 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 is not appropriate, you can override the valueEquals() method to implement your own comparison logic.
Warning
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 are not filled in, setModelValue() is not 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(), which 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 that can only be from 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 at 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 does not fire a value-change event.

If setModelValue() is called multiple times, the value of the last invocation is used, and it is not necessary 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.

Consider, for example, an employee selector field that allows the user to first select a department from a combo box, and then select an employee from the selected department in a second 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 is based directly on another field, while converting the value from that field.

The class declaration is a mix of Composite and AbstractField.

  1. The first type parameter defines the Composite content type, the second is for the value-change event getSource() type, and the third is the getValue() type of the field.

  2. 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:

  1. Configure departmentSelect value changes to update the items in employeeSelect.

  2. The constructor sets the employee selected in employeeSelect as the field’s value.

  3. 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 provided 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 given below 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 implementation given below is similar to how required indicators are handled above, 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();
}