Documentation

Documentation versions (currently viewingVaadin 23)

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 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 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 back end.

Note that to function properly, 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 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 is also possible to set the Grid columns to display by property name. For this, you need to get the column objects in order 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 easily 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 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 on the back end and the UI server.

Currently, only Grid and ComboBox properly support lazy data binding.

Lazy data binding works as follows:

  1. 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.

  2. 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 next to the user.

  3. Your callback methods use this Query object to fetch only the required data (usually from the back end) 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
);
  1. 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.

  2. Typically, you call your service layer from the callback, as is done here.

  3. The offset refers to the first index of the item to fetch.

  4. The limit refers to the number of items to fetch. When fetching for more data, you should utilize Query properties to litmit the amount of data to fetch.

  5. In this example, it is assumed that the back end returns a List, so we need to convert it to a Stream.

The example above works well with JDBC back ends, where you can request a set of rows from a given index. Vaadin executes your data binding call in paged manner, so it is possible to bind also to "paging back ends", 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
});
  1. Call a Spring Data repository to obtain the requested result set.

  2. The query object contains a shorthand for a zero-based page index.

  3. The query object also contains page size.

  4. Return a stream of items from the Spring Data Page object.

Sorting with Lazy Data Binding

For efficient lazy data binding, sorting needs to have already been done at 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(VaadinSpringDataHelpers.fromPagingRepository(repo)); 3
}
  1. If you are using property-name-based column definition, Grid columns can be made sortable by their property names. The setSortableColumns() method makes columns with given identifiers sortable and all others non-sortable.

  2. Alternatively, define a key to your columns, which will be passed to the callback, and define the column to be sortable.

  3. 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 a Vaadin Spring Data utility method to convert the values. This utility method also passes the sort information to our back-end call and returns the constructed callback. If you are using DTOs or otherwise want to customize binding to a Spring Data-based back end, the VaadinSpringDataHelpers class also contains toSpringPageRequest() and toSpringDataSort() methods to convert Vaadin query hints to their corresponding Spring Data relatives.

Filtering with Lazy Data Binding

Note that, for the lazy data to be efficient, filtering needs to be done at the back end. 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());
}
  1. 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.

  2. When a value-change event occurs, you should reset the data binding to use the new filter.

  3. The example back end uses SQL behind the scenes, so the filter string is wrapped in % characters to match anywhere in the text.

  4. Pass the filter to your back end 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 is not needed in order 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 will fetch more items as the user scrolls the dropdown, until there are no more items returned. If you want 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 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));
  1. Query object contains the filter of type returned by given converter.

  2. The second callback is used to convert the filter from the combo box text on the client side into an appropriate value for the back end.

Improving Scrolling Behavior

With simple lazy data binding, the component does not 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
  1. When assigning the callback, a data view object is returned. This can be configured directly or saved for later adjustments.

  2. 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.

  3. 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 1,500 items in the Grid.

A count callback has to be provided in order to get a similar user experience to that of assigning data directly. Note that in many back ends, 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 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. For example, particularly with lazy data binding, calling 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 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 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 finer-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 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 obtain an item count by hooking to the item count change event or request the item count directly.

For example, it is 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 of In-memory Data

Let us 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()));

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 is 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 will be essential for the component to work properly. For example, if you bind a list of Person objects to a Grid, the Grid will rely 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.

It is, therefore, 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 is 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 will not change, and a component like the Grid will still be 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