title | order | layout |
---|---|---|
Binding Items to Components |
5 |
page |
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, or components especially 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 a number of overloaded setItems()
methods to define the items to display.
Items can be basic objects, such as strings or numbers, or POJOs, such as DTOs or JPA entities, from your domain model.
The easiest way is to provide a List
of objects to be shown in such a component.
If the number of items is large and reserves a lot of memory, Grid
and
ComboBox
allow you to do a lazy data binding using callbacks to fetch only the required set of items from the back end.
Component-specific APIs allow adjusting how items are displayed.
Listing components use the toString()
method to display items by default.
If this is not 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 possible to define the Grid
columns to display also by the property names.
In this case, 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 how the listed data is displayed.
The easiest way to pass data to the listing component is to use an array or List
.
You can create those easily by 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());
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 back end 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 in the back end and the UI server. The component gives a query object as a parameter to your callback methods, where you can check that what part of the data set is needed.
Currently, only Grid
and ComboBox
properly support lazy data binding.
For example, to bind data lazily to a Grid
:
grid.setItems(query -> // (1)
getPersonService() // (2)
.fetchPersons(query.getOffset(), query.getLimit()) // (3)
.stream() // (4)
);
-
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.
-
Use the query object’s parameters to limit the data you pass from the back end to the component.
-
The callbacks return the data as a
Stream
. In this example, the back end returns aList
, so we need to convert it to aStream
.
The example above works well with JDBC back ends, where you can request a set of rows from a given index. Vaadin calls your data binding call in paged manner, so it is possible to bind also to the back ends with paging, 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 obtain the requested result set.
-
The query object contains a shorthand for zero-based page index.
-
The query object also contains page size.
-
Return a stream of items from the Spring Data
Page
object.
For efficient lazy data-binding, sorting needs to be done already in the back end.
By default, Grid
makes all columns appear sortable in the UI.
You need to manually declare which columns are actually sortable.
Otherwise, the UI may indicate that some columns are sortable, but nothing happens if you try to sort them.
With 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 -> {
Sort springSort = toSpringDataSort(query.getSortOrders()); // (3)
return repo.findAll(
PageRequest.of(
query.getPage(),
query.getPageSize(),
springSort // (4)
)).stream();
});
}
/**
* A method to convert given Vaadin sort hints to Spring Data specific sort
* instructions.
*
* @param vaadinSortOrders a list of Vaadin QuerySortOrders to convert to
* @return the Sort object for Spring Data repositories
*/
public static Sort toSpringDataSort(List<QuerySortOrder> vaadinSortOrders) {
return Sort.by(
vaadinSortOrders.stream()
.map(sortOrder ->
sortOrder.getDirection() == SortDirection.ASCENDING ?
Sort.Order.asc(sortOrder.getSorted()) : // (5)
Sort.Order.desc(sortOrder.getSorted())
)
.collect(Collectors.toList())
);
}
-
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 other non-sortable. -
Alternatively, define a key to your columns, which will be 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 back end understands. In this example, we are using Spring Data and using a separate method to convert the values. The method body is shown below. Note that the conversion becomes simpler if you only want to support sorting based on a single property. Vaadin Grid supports sorting based on multiple columns.
-
Here the back-end compatible sort information is passed to our back-end call.
-
The
getSorted()
method inQuerySortOrder
returns the columns property name or a key you have assigned to the column.
Note that, for the lazy data to be efficient, filtering needs to be done in the back end.
For example, 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 back end 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 back end uses SQL behind the scenes, so
%
is appended to the beginning and to the end to match anywhere in the text. -
Pass the filter to your back end in the binding.
You can combine both filtering and sorting in your data binding callbacks.
Let’s 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 end 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 is not needed for fetching 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 for lazy loading items, and the ComboBox
will fetch more items as the user scrolls the dropdown, until there are no more items returned.
In case it is desired to have the dropdown’s scrollbar reflect the exact number of items matching the filter, an optional item count callback can be used as shown in the example below:
ComboBox<Person> cb = new ComboBox<>();
cb.setItems(
query -> repo.findByNameLikeIgnoreCase(
"%" + query.getFilter().orElse("") + "%",
PageRequest.of(query.getPage(), query.getPageSize()))
.stream(),
query -> (int) repo.countByNameLikeIgnoreCase(
"%" + query.getFilter().orElse("") + "%"));
In case the filtering of items should be done with another type than a string, you can provide a filter converter with the fetch callback to get the type of the filter right 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 combo box’s text client-side filter into the appropriate value, used by back end.
In the case of the simple lazy data binding, the component does not know how many items there 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, those 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 giving an estimate or 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. It can be configured directly or saved for later adjustments.
-
If you know a rough estimate or rows, giving that to the component increases the user experience. Users can, for example, 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 back end returns an item for index 1000, the scrollbar is adjusted as if there were 1500 items in theGrid
.
A count callback has to be provided to get similar user experience as when assigning data directly. Note that in many back ends, counting the number of results can be a heavy operation.
dataView.setItemCountCallback(q -> getPersonService().getPersonCount());
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 is 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.
Especially with lazy data binding, calling for example grid.getGenericDataView().getItems() will cause the whole data set to be loaded from the back end.
|
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 is possible then to get the next or previous item of a certain item.
This can be done, of course, by saving the original data structure,
but this way you can implement a generic UI logic without dependencies to 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)
);
});
});
A typical scenario in Vaadin apps is that data displayed in, for example, a Grid
component, is edited elsewhere in the application.
Editing the item elsewhere does not 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 more fine-grained APIs in the DataView
to update just 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 the DataView::refreshItem
. This
would modify only one particular 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);
});
If you have bound a mutable List
to your component, you can alternatively use helper methods in the list data view to add or remove items or obtain item count by hooking to item count change event or request the item count directly.
For example, it’s possible to use a mutation methods and listening to 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());
Let’s consider the Grid
as an example of component with a sorting API.
Grid
rows are automatically sortable by columns that have property type that
implements Comparable
.
By defining a custom Comparator
, you can make also other columns sortable,
or you can override the default behavior for columns with comparable types.
For example, to make sorting string-typed columns case-insensitive:
grid.addColumn(Person::getName)
.setHeader("Name")
// Override the default sorting
.setComparator(Comparator.comparing(person ->
person.getName().toLowerCase()));
Note that 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
can be added or removed completely as well.
For example, you can define a 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();
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();
In large applications, you typically have multiple places where you display the same data type in a listing component. Multiple approaches can be used 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 allows sharing also other common configuration.
@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);