Binding Items to Components
You often have lists of items that you want to display in your application, and may want to let the user select one or more of them.
You can use basic components such as HTML elements to display such lists.
Alternatively, you can use components specially designed for the purpose, such as Grid
, ComboBox
, and ListBox
.
// Create a listing component for a bean type
Grid<Person> grid = new Grid<>(Person.class);
// Sets items using vararg beans
grid.setItems(
new Person("George Washington", 1732),
new Person("John Adams", 1735),
new Person("Thomas Jefferson", 1743),
new Person("James Madison", 1751)
);
All listing components in Vaadin have many overloaded setItems()
methods to define the items to display.
Items can be basic objects, such as strings or numbers, or plain-old Java objects (POJO),such as Data Transfer Objects (DTO) and JPA entities.
The easiest way to bind items to a component is to provide a List
of objects to be shown in such a component.
If there are many items, requiring a lot of memory, Grid
and ComboBox
allow lazy data binding using callbacks to fetch only the required set of items from the backend.
To function, components such as the Grid
and ComboBox
require that the assigned items have identifiers that are stable and unique.
This usually requires a proper implementation of the hashCode()
and equals()
methods as described in the section.
Configuring How Items Are Displayed
Component-specific APIs allow you to adjust how items are displayed.
By default, listing components use the toString()
method to display items.
If this isn’t suitable, you can change the behavior by configuring the component.
Listing components have one or more callbacks that define how to display the items.
For example, consider the ComboBox
component that lists status items.
You can configure it to use Status::getLabel()
method to get a label for each status item.
ComboBox<Status> comboBox = new ComboBox<>();
comboBox.setItemLabelGenerator(Status::getLabel);
In a Grid
, you can use addColumn()
to define the columns and configure the getter that returns the content for the column.
The setHeader()
method sets the column header.
// A bean with some fields
final class Person implements Serializable {
private String name;
private String email;
private String title;
private int yearOfBirth;
public Person(String name, int yearOfBirth) {
this.name = name;
this.yearOfBirth = yearOfBirth;
}
public String getName() {
return name;
}
public int getYearOfBirth() {
return yearOfBirth;
}
public String getTitle() {
return title;
}
// other getters and setters
}
// Show such beans in a Grid
Grid<Person> grid = new Grid<>();
grid.addColumn(Person::getName)
.setHeader("Name");
grid.addColumn(Person::getYearOfBirth)
.setHeader("Year of birth");
It’s also possible to set the Grid
columns to display by property name.
For this, you need to get the column objects to configure the headers.
Grid<Person> grid = new Grid<>(Person.class);
grid.setColumns("name", "email", "title");
grid.getColumnByKey("name").setHeader("Name");
grid.getColumnByKey("email").setHeader("Email");
grid.getColumnByKey("title").setHeader("Title");
Check the component examples for more details on configuring the display of listed data.
Assigning a List or Array of In-Memory Data
The easiest way to pass data to the listing component is to use an array or List
.
You can create these yourself or pass values directly from your service layer.
Example: Passing in-memory data to components using the setItems()
method.
// Sets items as a collection
List<Person> persons = getPersonService().findAll();
comboBox.setItems(persons);
// Sets items using vararg beans
grid.setItems(
new Person("George Washington", 1732),
new Person("John Adams", 1735),
new Person("Thomas Jefferson", 1743),
new Person("James Madison", 1751)
);
// Pass all Person objects to a grid from a Spring Data repository object
grid.setItems(personRepository.findAll());
Lazy Data Binding Using Callbacks
Using callback methods is a more advanced way to bind data to components. This way, only the required portion of the data is loaded from your backend to the server memory. This approach is harder to implement and provides fewer features out of the box, but can save a lot of resources on the backend and the UI server.
Currently, only Grid
and ComboBox
support lazy data binding.
Lazy data binding works as follows:
-
The user performs an action that requires the component to display more data. For example, the user might be scrolling down the list of items in a
Grid
component. -
The component automatically detects that more data is needed, and it passes a
Query
object as a parameter to your callback methods. This object contains the necessary information about the data that should be displayed to the user next. -
Your callback methods use this
Query
object to fetch only the required data (usually from the backend) and return it to the component, which displays it automatically once the data is available.
For example, to bind data lazily to a Grid
:
grid.setItems(query -> // (1)
getPersonService() // (2)
.fetchPersons(
query.getOffset(), // (3)
query.getLimit() // (4)
)
.stream() // (5)
);
-
To create a lazy binding, use an overloaded version of the
setItems()
method that uses a callback instead of passing data directly to the component. -
Typically, you call your service layer from the callback, as is done here.
-
The offset refers to the first index of the item to fetch.
-
The limit refers to the number of items to fetch. When fetching more data, you should utilize
Query
properties to limit the amount of data to fetch. -
In this example, it’s assumed that the backend returns a
List
, so you need to convert it to aStream
.
The example above works well with JDBC backends, where you can request a set of rows from a given index. Vaadin executes your data binding call in paged manner, so it’s also possible to bind to "paging backends", such as Spring Data-based solutions.
For example, to do lazy data binding from a Spring Data Repository to Grid
:
grid.setItems(query -> {
return repository.findAll( // (1)
PageRequest.of(query.getPage(), // (2)
query.getPageSize()) // (3)
).stream(); // (4)
});
-
Call a Spring Data repository to get the requested result set.
-
The query object contains a shorthand for a zero-based page index.
-
The query object also contains page size.
-
Return a stream of items from the Spring Data
Page
object.
Sorting with Lazy Data Binding
For efficient lazy data binding, sorting needs to happen in the backend.
By default, Grid
makes all columns appear sortable in the UI if you pass the class as a constructor parameter.
You can manually declare which columns should be sortable.
Otherwise, the UI may show that some columns are sortable, but nothing happens if you try to sort them.
To make sorting work in a lazy data binding, you need to pass the hints that Grid
provides in the Query
object to your back-end logic.
For example, to enable sortable lazy data binding to a Spring Data repository:
public void bindWithSorting() {
Grid<Person> grid = new Grid<>(Person.class);
grid.setSortableColumns("name", "email"); // (1)
grid.addColumn(person -> person.getTitle())
.setHeader("Title")
.setKey("title").setSortable(true); // (2)
grid.setItems(query -> { // (3)
var vaadinSortOrders = query.getSortOrders();
var springSortOrders = new ArrayList<Sort.Order>();
for (QuerySortOrder so : vaadinSortOrders) {
String colKey = so.getSorted();
if(so.getDirection() == SortDirection.ASCENDING) {
springSortOrders.add(Sort.Order.asc(colKey));
}
}
return repository.findAll(
PageRequest.of(
query.getPage(),
query.getPageSize(),
Sort.by(springSortOrders)
)
).stream();
});
}
-
If you are using property-name-based column definition,
Grid
columns can be made sortable by their property names. ThesetSortableColumns()
method makes columns with given identifiers sortable and all others non-sortable. -
Alternatively, define a key to your columns, which is passed to the callback, and define the column to be sortable.
-
In the callback, you need to convert the Vaadin-specific sort information to whatever your backend understands. This example uses Spring Data based backend, so it is mostly converting Vaadin’s QuerySortOrder hints to Spring’s
Order
objects and finally passing the sort (and paging) details to the backend.
Note
|
Helpers for Spring Data based backends
The examples above are written against Spring Data based examples, but in a verbose way to keep them relevant for any kind of Java backend service.
If you are using Spring Data based backends, the above code examples can be written with one-liners using the helper methods in VaadinSpringDataHelpers class.
It contains toSpringPageRequest() and toSpringDataSort() methods to automatically convert Vaadin specific query hints to their Spring Data relatives.
Using the fromPagingRepository() method, you can create a lazy sortable data binding directly to your repository.
|
Filtering with Lazy Data Binding
For the lazy data to be efficient, filtering needs to be done at the backend.
For instance, if you provide a text field to limit the results shown in a Grid
, you need to make your callbacks handle the filter.
For example, to handle filterable lazy data binding to a Spring Data
repository in Grid
:
public void initFiltering() {
filterTextField.setValueChangeMode(ValueChangeMode.LAZY); // (1)
filterTextField.addValueChangeListener(e -> listPersonsFilteredByName(e.getValue())); // (2)
}
private void listPersonsFilteredByName(String filterString) {
String likeFilter = "%" + filterString + "%";// (3)
grid.setItems(q -> repo
.findByNameLikeIgnoreCase(
likeFilter, // (4)
PageRequest.of(q.getPage(), q.getPageSize()))
.stream());
}
-
The lazy data binding mode is optimal for filtering purposes. Queries to the backend are only done when a user makes a small pause while typing.
-
When a value-change event occurs, you should reset the data binding to use the new filter.
-
The example backend uses SQL behind the scenes, so the filter string is wrapped in
%
characters to match anywhere in the text. -
Pass the filter to your backend in the binding.
You can combine both filtering and sorting in your data binding callbacks.
Consider a ComboBox
as an another example of lazy-loaded data filtering.
The lazy-loaded binding in ComboBox
is always filtered by the string typed in by the user.
Initially, when there is no filter input yet, the filter is an empty string.
The ComboBox
examples below use the new data API available since Vaadin 18, where the item count query isn’t needed to fetch items.
For example, you can handle filterable lazy data binding to a Spring Data repository as follows:
ComboBox<Person> cb = new ComboBox<>();
cb.setItems(
query -> repo.findByNameLikeIgnoreCase(
// Add `%` marks to filter for an SQL "LIKE" query
"%" + query.getFilter().orElse("") + "%",
PageRequest.of(query.getPage(), query.getPageSize()))
.stream()
);
The above example uses a fetch callback to lazy-load items, and the ComboBox
fetches more items as the user scrolls the drop-down, until there are no more items returned.
If you want the scrollbar in the drop-down to reflect the exact number of items matching the filter, an optional item count callback can be used, as shown in the following example:
cb.setItems(
query -> repo.findByNameLikeIgnoreCase(
"%" + query.getFilter().orElse("") + "%",
PageRequest.of(query.getPage(), query.getPageSize()))
.stream(),
query -> (int) repo.countByNameLikeIgnoreCase(
"%" + query.getFilter().orElse("") + "%"));
If you want to filter items with a type other than a string, you can provide a filter converter with the fetch callback to get the right type of filter for the fetch query:
ComboBox<Person> cb = new ComboBox<>();
cb.setPattern("\\d+");
cb.setPreventInvalidInput(true);
cb.setItemsWithFilterConverter(
query -> getPersonService()
.fetchPersonsByAge(query.getFilter().orElse(null), // (1)
query.getOffset(), query.getLimit())
.stream(),
textFilter -> textFilter.isEmpty() ? null // (2)
: Integer.parseInt(textFilter));
-
Query
object contains the filter of type returned by given converter. -
The second callback is used to convert the filter from the combo box text on the client side into an appropriate value for the backend.
Improving Scrolling Behavior
With lazy data binding, the component doesn’t know how many items are actually available.
When a user scrolls to the end of the scrollable area, Grid
polls your callbacks for more items.
If new items are found, these are added to the component.
This causes the relative scrollbar to behave in a strange way as new items are added on the fly.
The usability can be improved by providing an estimate of the actual number of items in the binding code.
The adjustment happens through a DataView
instance, which is returned by the setItems()
method.
For example, to configure the estimate of rows and how the "virtual row count" is adjusted when the user scrolls down:
GridLazyDataView<Person> dataView = grid.setItems(query -> { // (1)
return getPersonService()
.fetchPersons(query.getOffset(), query.getLimit())
.stream();
});
dataView.setItemCountEstimate(1000); // (2)
dataView.setItemCountEstimateIncrease(500); // (3)
-
When assigning the callback, a data view object is returned. This can be configured directly or saved for later adjustments.
-
If you have a rough estimate of rows, passing this to the component improves the user experience. For example, users can scroll directly to the end of the result set.
-
You can also configure how
Grid
adjusts its estimate of available rows. With this configuration, if the backend returns an item for index 1000, the scrollbar is adjusted as if there were 1,500 items in theGrid
.
A count callback has to be provided to get a similar user experience to that of assigning data directly. In many backends, counting the number of results can be an intensive operation.
dataView.setItemCountCallback(q -> getPersonService().getPersonCount());
Accessing Currently Shown Items
You may need to get a handle to all items shown in a listing component.
For example, add-ons or generic helpers might want to do something with the data that’s currently listed in the component.
For such a purposes, the supertype of data views can be accessed with the getGenericDataView()
method.
Caution
|
Calling certain methods in data views can be an expensive operation.
For example, particularly with lazy data binding, calling grid.getGenericDataView().getItems() causes the whole data set to be loaded from the backend.
|
For example, you can export persons listed in a Grid
to a CSV file as follows:
private void exportToCsvFile(Grid<Person> grid)
throws FileNotFoundException, IOException {
GridDataView<Person> dataView = grid.getGenericDataView();
FileOutputStream fout = new FileOutputStream(new File("/tmp/export.csv"));
dataView.getItems().forEach(person -> {
try {
fout.write((person.getFullName() + ", " + person.getEmail() +"\n").getBytes());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
});
fout.close();
}
If you have assigned your items as in-memory data, you have more methods available in a list data view object.
You can get the reference to that as a return value of the setItems()
method or through the getListDataView()
method.
It’s then possible to get the next or previous item to a certain item.
Of course, this can be done by saving the original data structure,
but this way you can implement a generic UI logic without dependencies on the assigned data.
For example, you can programmatically select the next item in a Grid
, if a current value is selected and there is a next item after it.
List<Person> allPersons = repo.findAll();
GridListDataView<Person> gridDataView = grid.setItems(allPersons);
Button selectNext = new Button("Next", e -> {
grid.asSingleSelect().getOptionalValue().ifPresent(p -> {
gridDataView.getNextItem(p).ifPresent(
next -> grid.select(next)
);
});
});
Updating the Displayed Data
A typical scenario in Vaadin applications is that data displayed in, for example, a Grid
component, is edited elsewhere in the application.
Editing the item elsewhere doesn’t automatically update the UI in a listing component.
An easy way to refresh the component’s content is to call setItems()
again with the fresh data.
Alternatively, you can use finer-grained APIs in the DataView
to update a portion of the dataset.
For example, you can modify one or more fields of a displayed item and notify
Grid
about the updates to the item through DataView::refreshItem()
.
This would modify only one specific item, not the whole data set.
Person person = new Person();
person.setName("Jorma");
person.setEmail("old@gmail.com");
GridListDataView<Person> gridDataView = grid.setItems(person);
Button modify = new Button("Modify data", e -> {
person.setEmail("new@gmail.com");
// The component shows the old email until notified of changes
gridDataView.refreshItem(person);
});
Alternatively, if you have bound a mutable List
to your component, you can use helper methods in the list data view to add or remove items.
You can also get an item count by hooking to the item count change event or request the item count directly.
For example, it’s possible to use a mutation method and listen for an item count change through the list data view, as follows:
// The initial data
ArrayList<String> items = new ArrayList<>(Arrays.asList("foo", "bar"));
// Get the data view when binding it to a component
Select<String> select = new Select<>();
SelectListDataView<String> dataView = select.setItems(items);
TextField newItemField = new TextField("Add new item");
Button addNewItem = new Button("Add", e -> {
// Adding through the data view API mutates the data source
dataView.addItem(newItemField.getValue());
});
Button remove = new Button("Remove selected", e-> {
// Same for removal
dataView.removeItem(select.getValue());
});
// Hook to item count change event
dataView.addItemCountChangeListener(e ->
Notification.show(" " + e.getItemCount() + " items available"));
// Request the item count directly
Span itemCountSpan = new Span("Total Item Count: " + dataView.getItemCount());
Sorting In-Memory Data
Consider the Grid
as an example of a component with a sorting API.
Grid
rows are automatically sortable by columns that have a property type that implements Comparable
.
By defining a custom Comparator
, you can also make other columns sortable.
Alternatively, you can override the default behavior of columns with comparable types.
For example, to make the sorting of string-typed columns case-insensitive:
grid.addColumn(Person::getName)
.setHeader("Name")
// Override the default sorting
.setComparator(Comparator.comparing(person ->
person.getName().toLowerCase()));
This kind of sorting is only supported for in-memory data. See Sorting with Lazy Data Binding for how to sort lazy-loaded data.
It’s possible to sort a collection of bound items with the DataView
API, either by setting a Comparator
or a sort order for a given bean field.
Sort orders or Comparator
instances can be added or removed, as well.
For example, you can define custom sorting through the DataView
API as follows:
// You get a DataView when setting the items
GridListDataView<Person> dataView = grid
.setItems(personRepository.findAll());
// Change the sort order of items collection
dataView.setSortOrder(Person::getName, SortDirection.ASCENDING);
// Add a secondary sort order to the existing sort order
dataView.addSortOrder(Person::getTitle, SortDirection.ASCENDING);
// Remove sorting completely (undoes the settings done above)
dataView.removeSorting();
Filtering In-Memory Data
If you are using an in-memory data set, you can also apply filters through the data view object. The filtered list is automatically updated to the UI.
For example, you can use a list data view to filter items based on a property as follows:
List<Person> allPersons = repo.findAll();
GridListDataView<Person> gridDataView = grid.setItems(allPersons);
// Filter Persons younger 20 years
gridDataView.setFilter(p -> p.getAge() < 20);
// Remove filters completely (undoes the settings done above)
gridDataView.removeFilters();
Recycling Data Binding Logic
In large applications, you typically have multiple places where you display the same data type in a listing component. You can use various approaches to share the lazy data binding logic.
One way is to use a domain-object-specific component implementation by extending a listing component to handle the application-specific data binding. This approach also allows you to share other common configuration aspects.
@SpringComponent
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PersonGrid extends Grid<Person> {
public PersonGrid(@Autowired PersonRepository repo) {
super(Person.class);
// Make the lazy binding
setItems(q -> repo.findAll(
PageRequest.of(q.getPage(), q.getPageSize())).stream());
// Make other common/default configuration
setColumns("name", "email");
}
}
You can also use a static helper method to bind the data as follows:
public static void listItems(Grid<Person> grid, PersonRepository repository) {
grid.setItems(query -> repository.findAll(
PageRequest.of(query.getPage(), query.getPageSize())).stream());
}
You can create a separate data provider class.
The following example uses only the FetchCallBack
, but you can also implement a full data provider by, for example, extending AbstractbackendDataProvider
.
@SpringComponent
public class PersonDataProvider implements CallbackDataProvider.FetchCallback<Person, Void> {
@Autowired
PersonRepository repo;
@Override
public Stream<Person> fetch(Query<Person, Void> query) {
return repo.findAll(PageRequest.of(query.getPage(),
query.getPageSize())).stream();
}
}
personGrid.setItems(dataProvider);
Ensuring That Item Identities Are Stable and Unique
When you bind items to a component, the identities of those items are essential for the component to work.
For example, if you bind a list of Person
objects to a Grid
, the Grid
relies on the identities of the Person
objects for various operations, such as for highlighting the selected rows and updating the data in an edited row.
For that reason, it’s important that the identities of the items are stable and unique.
To ensure that, your Java class should implement hashCode()
and equals()
methods based on the invariant properties of the class.
For example, the Person
class in the following example has a final long id
property that’s used in the hashCode()
and equals()
methods as the identity of the Person
object.
This way even if the Person
object has a property such as its phoneNumber
modified, the identity of the Person
object doesn’t change, and a component like the Grid
is still able to identify the object correctly.
class Person {
private final long id;
private String name;
private String phoneNumber;
public Person(long id, String name, String phoneNumber) {
this.id = id;
this.name = name;
this.phoneNumber = phoneNumber;
}
// Getters and setters omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id == person.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
If you are using the Lombok library to generate hashCode()
and equals()
methods, you need to ensure that only the invariant properties are being used.
You can do this by setting onlyExplicitlyIncluded = true
for @EqualsAndHashCode
, and then annotating the entity’s id
property with @EqualsAndHashCode.Include
.
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
class Person {
@EqualsAndHashCode.Include
private final long id;
private String name;
private String phoneNumber;
public Person(long id, String name, String phoneNumber) {
this.id = id;
this.name = name;
this.phoneNumber = phoneNumber;
}
// Getters and setters omitted for brevity
}
8D0BFB55-CF96-456D-9312-9018D9413CA2