Documentation

Documentation versions (currently viewingVaadin 24.4 (pre))

Collaborative Form Editing

The recommended way of binding data from Java beans to forms in Vaadin applications is to use Binder (see Binding Data to Forms). Collaboration Kit provides a Binder extension called, CollaborationBinder.

This extension adds field value synchronization and field highlight to the binder’s data binding and validation APIs. With field value synchronization, when a user changes a field’s value, it’s updated for the other users. As for field highlight, when a user is focused on a field, other users see a highlight around the field, along with the name of the user who is editing it.

A form showing the email field is being edited by another user
Collaborative Form Bound with CollaborationBinder

Constructing CollaborationBinder

The constructor of CollaborationBinder requires the bean type on which to bind values. This is similar to the regular Binder when binding by property names. As the second argument, you’d provide information about the end user.

The same UserInfo object is required by all Collaboration Kit features. CollaborationBinder uses the name in UserInfo, when indicating that another user is editing a field with the field highlight.

User userEntity = userService.getCurrentUser();

UserInfo userInfo = new UserInfo(userEntity.getId(),
        userEntity.getName());

CollaborationBinder<Person> binder = new CollaborationBinder<>(
        Person.class, userInfo);

Connecting & Populating

The CollaborationBinder::setTopic method serves to connect to Collaboration Kit in the scope of the edited item, and to populate the form with initial values loaded from a backend.

When selecting an item to edit (e.g., an instance of Person bean class), setTopic is used to connect to the topic and to populate the form:

public void personSelected(long personId) {
    binder.setTopic("person/" + personId,
            () -> personService.findById(personId));
}

The first parameter is the identifier of the topic on which to connect. Using unique topic identifiers for different items ensures edits on different items don’t interfere with each other.

The second parameter is a callback for providing the bean that populates the fields. The regular Binder has the readBean method for populating the fields based on bean properties. This method, though, isn’t supported by CollaborationBinder because calling readBean when a new view instance is constructed would have an unwanted effect: Every time a new user would join in editing the form, the field values would reset for every user. The setBean method is unsupported for the same reason.

The callback provided for setTopic replaces readBean. It’s used to populate the form if the topic doesn’t have any data, which means the user is the first to edit it. Otherwise, the field values are loaded from the topic in Collaboration Kit. In this case, the callback isn’t even called, possibly avoiding an unnecessary database request.

Note
If you want to override all field values for all collaborators (i.e., implement a reset button), you can use CollaborationBinder::reset method. It takes a bean instance and uses its properties for setting the field values.

Binding Collaborative Fields

The example here shows how you can bind the name property for the Person bean to a text field. This also enables the collaborative features (i.e., value synchronization and field highlight).

TextField name = new TextField();
binder.forField(name).bind("name");
CheckboxGroup<String> pets = new CheckboxGroup<>();
pets.setItems("Dog", "Cat", "Parrot");

binder.forField(pets, Set.class, String.class).bind("pets");

You could write exactly the same code for the regular Binder. CollaborationBinder adds the collaborative features in addition to regular data binding. Binding based on a property name (i.e., "name" in this case) requires the bean class (i.e., Person) to have standard getter and setter methods: getName and setName.

The other bind variant, which takes the getter and setter callbacks as arguments, isn’t supported by CollaborationBinder. The reason for this is that a unique key is needed for each field or property to store the data in the underlying CollaborationMap data structure. The property name is required for that purpose, to be used as the key.

Because the data used to communicate with Collaboration Kit is serialized as JSON, there are some limitations to what CollaborationBinder can do automatically. To cover the special cases, you need to do a little bit more than bind a property to a field.

Non-Primitive Value Types

Collaboration Kit supports only a limited set of primitive-like value types, types that it knows how to serialize and deserialize. When using some other field value type, you must explicitly provide the serializer and deserializer functions.

When the field is used for selecting a bean object that has a unique identifier, you can serialize the value by converting the bean to its identifier, and deserialize it by fetching the bean object that matches the identifier.

In this example, the user can change the supervisor for an employee from a list of supervisors. Both the employee and the supervisor are distinct Person beans. Use a ComboBox component for selecting the person’s supervisor:

ComboBox<Person> supervisor = new ComboBox<>();
supervisor.setItems(personService.findAllSupervisors());

binder.setSerializer(Person.class,
        person -> String.valueOf(person.getId()),
        id -> personService.findById(Long.parseLong(id)));

binder.bind(supervisor, "supervisor");

The person identifiers are stored as longs in this case, and the serialized value needs to be a String. You’d need to do a bit of converting, though, between strings and longs.

Converters

When a Converter is used, you must provide the field’s value type in forField.

The example here binds an enum property of the bean to a Checkbox. Therefore, the value type boolean needs to be provided:

Checkbox married = new Checkbox();
binder.forField(married, Boolean.class)
        .withConverter(
                fieldValue -> fieldValue ? MaritalStatus.MARRIED
                        : MaritalStatus.SINGLE,
                MaritalStatus.MARRIED::equals)
        .bind("maritalStatus");

This is necessary because CollaborationBinder uses the bean property type (i.e., the enum called MaritalStatus) for deserializing the field value, by default.

Multi-Select Fields

When the field’s value type is a collection, you must provide the type of the collection, as well as the type of its contents in forField.

The value type of CheckboxGroup is Set in the following example. Here you must provide the collection type, Set, and the content type, String.

TextField name = new TextField();
binder.forField(name).bind("name");
CheckboxGroup<String> pets = new CheckboxGroup<>();
pets.setItems("Dog", "Cat", "Parrot");

binder.forField(pets, Set.class, String.class).bind("pets");

This is necessary because CollaborationBinder can’t infer automatically the generic type for deserializing the value. If the element type isn’t supported by Collaboration Kit (e.g., CheckboxGroup<Person>), you’ll still need to implement custom serializer and deserializer functions.

Eagerly Propagate Values

On text fields, the default and recommended mode for propagating values from one user to others is when the user blurs the field, or when the user presses the Enter key. You can configure how eagerly the field sends data through its own API, using the setValueChangeMode(ValueChangeMode) method.

For example, to send instantly each keystroke to other users, you would do the following:

TextField textField = new TextField();
textField.setValueChangeMode(ValueChangeMode.EAGER);

Modes like ValueChangeMode.LAZY and ValueChangeMode.TIMEOUT can also be used together with the setValueChangeTimeout(int) method to reduce the amount of traffic.

Resetting When No Editors

By default, the edited field values are stored in Collaboration Kit, indefinitely. Even a long time after someone viewed a form, when it’s opened you’re presented with field values from the previous user. For such situations, you might instead want the field values to be re-populated from the backend.

This can be achieved by setting an expiration timeout on the binder. If there haven’t been any connected users for the set period of time, the binder calls the bean supplier callback — provided in setTopic — to repopulate the fields.

binder.setExpirationTimeout(Duration.ofMinutes(15));

You can use Duration.ZERO to reset the fields immediately when there’s a moment with no connected users. However, it might be better to have a small time window before it does this. It allows a user to recover from a temporary network issue without having to start over editing the form.

62AA4352-774C-4E38-9D67-A8AA67DA8781