I'm tring to figure out how to bind meta data about an entity that's not an actual field in the entity. For example, consider the following:
Task:
title: string
description: string
...
notes: List<TaskNote>
I'd like to be able to display the Task
in a grid, or in an "editor" form and display, say the count of TaskNote
s associated with a Task
. So, in this case, the count is simply the number of items in the notes
list.
How do I create a data binding for something like this? What are the best practices/techniques?
A Binder is used for binding Fields
into a specific bean's Properties
. A Person
bean could have the Properties
"first name", "last name" and "birth year". However, Field
and Property
are quite abstract concepts, leaving you a lot of room to maneuver.
Field
is something, typically a Component
, that implements the HasValue
interface. The HasValue
interface (and thus the Field
) has a Java type, such as String
.Property
is essentially a getter/setter pair. The pair must have a matching type - the getter's return type must match what the setter takes as an input.
Property
's getter can be provided by implementing the ValueProvider
functional interface, which, in a simple form, can be a Lambda expression such as person -> person.getFirstName()
Setter
functional interface. A simple example as a Lambda would be (person, newFirstName) -> person.setFirstName(newFirstName)
.When you create a Binder
, you provide the ValueProvider
and Setter
in one way or another; the most explicit way would be when you're binding like this:
Binder<Person> binder = new Binder<>();
TextField firstNameField = new TextField("First name");
binder.forField(firstNameField).bind(valueProvider, setter);
When you're dealing with something that is a part of a bean, but not a Property
, you have a couple of different options. For example, the Person's "age" can be calculated as a function of their "birth year". Let's say we only want to be able to edit the birth year, but we also want to display the age as a read-only value. It could also be a discount percentage based on age or something.
The first, easiest approach is to use a Field
, but only with a ValueProvider
and no Setter
. So you could have something like this (using TextField
for simplicity):
Binder<Person> binder = new Binder<>();
TextField ageField = new TextField("Age (calculated)");
binder.forField(ageField).bind(person -> calculateAge(person.getBirthYear()), null);
The downside of this approach is that you're still using a TextField
, which is an editor component. You can set it to read-only mode so the user can't change the value, but it still looks like you might be able to edit it at some point.
Another thing you can use is a ReadOnlyHasValue
, which is an interface for creating a Property
without a Setter
. It's a bit clunky, but you can use it with binding:
Span ageDisplay = new Span();
ReadOnlyHasValue<String> ageField = new ReadOnlyHasValue<>(text -> ageDisplay.setText(text));
binder.forField(ageField).bind(person -> calculateAge(person.getBirthYear()), null);
Finally, as ValueProvider
and Setter
are just Java interfaces, you can implement them with any kinds of side effects that you might want. So you can piggyback any logic to a Property's changes:
Binder<Person> binder = new Binder<>();
Span ageDisplay = new Span();
TextField birthYear = new TextField();
binder.forField(birthYear)
.bind(person -> { // ValueProvider
return person.getBirthYear();
}, (person, newBirthYear) -> { // Setter
person.setBirthYear(newBirthYear);
ageDisplay.setText(calculateAge(newBirthYear));
});