Collaborative Form Editing
The recommended way of binding data from Java beans to forms in Vaadin applications is to use Binder
(read Binding Data to Forms to learn more).
Collaboration Engine provides a Binder
extension called CollaborationBinder
,
which adds the following real-time features on top of the binder’s data binding and validation APIs:
-
Field value synchronization: When a user enters a new value to a field, the value is updated for the other users as well.
-
Field highlight: When a user is focused on a field, other users see a highlight around the field and a user name indicating who is currently editing it.
CollaborationBinder
Constructing a CollaborationBinder
The constructor of CollaborationBinder
requires the bean type to bind values to,
similarly to the regular Binder
when binding by property names.
As the second argument, you must provide some information about the end user.
The same UserInfo
object is required by all Collaboration Engine 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 to Collaboration Engine and Populating the Form
The CollaborationBinder::setTopic
method serves two purposes:
-
Connecting to Collaboration Engine in the scope of the edited item
-
Populating the form with initial values loaded from a backend
Here is an example use case. When selecting an item to edit (an instance of Person
bean class in this case),
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 id of the topic to connect to. By using unique topic IDs for different items, we make sure that 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 is not supported by CollaborationBinder
. The reason is, that calling readBean
when a new view instance is constructed would have an unwanted effect:
Every time a new user would join 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 will be used to populate the form if the topic doesn’t have any data yet,
which means that the user is the first one to edit that form.
Otherwise, the field values are loaded from the topic in Collaboration Engine.
In this case, the callback is not even called, possibly avoiding an unnecessary database request.
Note
|
If you explicitly want to override all field values for all collaborators (for example implement a reset button),
you can use CollaborationBinder::reset method, which takes a bean instance and uses its properties for
setting the field values.
|
Binding Collaborative Fields
Here is an example of how we can bind the Person
bean’s name
property to a text field,
enabling also the collaborative features (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 with the regular Binder
.
CollaborationBinder
just adds the collaborative features on top of the regular data binding.
Note that binding based on a property name ("name" in this case) requires the bean class (Person
)
to have standard getter and setter methods getName
and setName
.
The other bind
variant, which takes the getter and setter callbacks as arguments, is not supported by CollaborationBinder
.
The technical reason for this is, that some unique key is needed
per each field/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 Engine is serialized as JSON, there are some limitations
to what CollaborationBinder
can do automatically.
Next, we’ll cover the special cases where you need to do a little bit more than
simply binding a property to a field.
Non-Primitive Value Types
Collaboration Engine supports only a limited set of primitive-like value 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 by fetching the bean object that matches the id.
In this example, the Person
bean that we are editing, has a reference to the person’s
supervisor, which is another Person
. We’ll 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 ids are stored as longs
in this case, and the serialized value needs to be a String
.
In this case, we need to do a bit of converting between strings and longs.
Converters
When a Converter
is used, you must provide the field’s value type in forField
.
In this example, we are binding an enum property of the bean to a Checkbox
,
so the checkbox’s 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 (MaritalStatus
enum in this case) 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
.
For example, the value type of CheckboxGroup
is Set
.
In the example below, we 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 automatically infer the
generic type for deserializing the value.
Note that if the element type is not supported by Collaboration Engine (for example CheckboxGroup<Person>
),
you need to implement custom (de)serializer functions anyway.
Propagate Values Eagerly in Text Fields
On text fields, the default and recommended mode for propagating values from one user to others is when the user blurs the field or 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 instantly send 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 Fields When There Are No Editors
By default, the edited field values are stored in Collaboration Engine indefinitely. Even after a long time has passed since someone has visited a form and you open the form again, you will be presented with the field values that the previous user might have left there. In this scenario, you might want the field values to be re-populated from the backend instead.
This can be achieved by setting an expiration timeout on the binder.
If there hasn’t been any connected users for the set period of time,
binder will call the bean supplier callback (provided in setTopic()
)
again to populate 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 a good idea to have a small time window.
This enables, for example, a user to recover from a temporary network issue
without having to start over with the form editing.
6272DDD1-CF5A-4076-A265-275176ADD560