Skip to content

Commit

Permalink
docs: update data-provider article for item index provider (#3089) (#…
Browse files Browse the repository at this point in the history
…3117)

* docs: update data-provider article for item index provider

Document how to make callback for an item index with lazy data binding.

Related-to: vaadin/flow#18088

* docs: fix vale findings

* First pass at editing.

* Vale fix

* Second full pass at editing.

---------

Co-authored-by: Russell J.T. Dyer <6652767+russelljtdyer@users.noreply.github.com>
Co-authored-by: Russell JT Dyer <russelljtdyer@users.noreply.github.com>
Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
  • Loading branch information
4 people authored Jan 12, 2024
1 parent 19be062 commit ffa5e2d
Showing 1 changed file with 66 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
title: Binding Items to Components
description: How to bind and display a list of items in components such as Grid and Combo Box.
description: Binding and displaying a list of items in components, such as Grid and Combo Box.
order: 50
---


= Binding Items to Components

Selection components allow selecting a field value from a list of options. This article describes how they're bound and displayed in such components.
Selection components allow selecting a field value from a list of options. This page describes how they're bound and displayed in such components.

Applications often display lists of items. You might want users to select one or more of these items. To display such lists, you can use basic components such as HTML elements. Alternatively, you can use components specifically designed for this purpose. For example, there's `Grid`, `ComboBox`, and `ListBox`.
Applications often display lists of items, of which you might want users to select one or more items. To display such lists, you can use basic components such as HTML elements. Alternatively, you can use components specifically designed for this purpose: such as `Grid`, `ComboBox`, and `ListBox`.

[source,java]
----
Expand All @@ -25,9 +25,9 @@ grid.setItems(
);
----

All listing components in Vaadin have many overloaded [methodname]`setItems()` methods to define the items to display. Items can be basic objects, such as strings or numbers, or they can be plain-old Java objects (POJO), such as Data Transfer Objects (DTO) and JPA entities.
All listing components in Vaadin have many overloaded [methodname]`setItems()` methods to define the items to display. Items can be basic objects (e.g., strings or numbers), or they can be 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 [classname]`List` of objects to be shown in such a component.
The easiest way to bind items to a component is to provide a [classname]`List` of objects to be shown in the component.

If there are many items, requiring plenty of memory, `Grid` and `ComboBox` allow lazy data binding using callbacks to fetch only the required set of items from the backend.

Expand All @@ -38,15 +38,15 @@ include::{articles}/flow/binding-data/_items-identities.adoc[]

Component-specific APIs allow you to adjust how items are displayed. By default, listing components use the [methodname]`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 [methodname]`Status::getLabel()` method to get a label for each status item.
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 [methodname]`Status::getLabel()` method to get a label for each status item:

[source,java]
----
ComboBox<Status> comboBox = new ComboBox<>();
comboBox.setItemLabelGenerator(Status::getLabel);
----

In a `Grid`, you can use [methodname]`addColumn()` to define the columns and configure the getter that returns the content for the column. The [methodname]`setHeader()` method sets the column header.
In a `Grid`, you can use [methodname]`addColumn()` to define the columns and configure the getter that returns the content for the column. The [methodname]`setHeader()` method sets the column header:

[source,java]
----
Expand Down Expand Up @@ -85,7 +85,7 @@ 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.
It's also possible to set the `Grid` columns to display by property name. For this, you'll need to get the column objects to configure the headers:

[source,java]
----
Expand Down Expand Up @@ -126,15 +126,13 @@ grid.setItems(personRepository.findAll());

== Lazy Data Binding Using Callbacks

Using callback methods is a more advanced way to bind data to components. By this method, only the required portion of the data is loaded from your backend to the server memory. This approach is more difficult to implement and provides fewer features out-of-the-box, but it can save plenty of resources on the backend and on the UI server.
Using callback methods is a more advanced way to bind data to components. By this method, only the required portion of the data is loaded from your backend to the server memory. This approach is more difficult to implement and provides fewer features out-of-the-box. However, it can save plenty of resources on the backend and on the UI server.

Currently, only `Grid` and `ComboBox` support lazy data binding.
Currently, only `Grid` and `ComboBox` support lazy data binding. It's important to understand how lazy data binding works. Here's the process it follows:

It's important to understand how lazy data binding works. Here's the process it follows:

. The user performs an action that requires the component to display more data. For example, the user might scroll down a list of items in a `Grid` component.
. The component automatically detects that more data is needed, and it passes a link:https://vaadin.com/api/platform/{moduleMavenVersion:com.vaadin:vaadin}/com/vaadin/flow/data/provider/Query.html[`Query`] object as a parameter to the callback methods. This object contains the necessary information about the data that should be displayed next to the user.
. The callback methods use this [classname]`Query` object to fetch only the required data -- usually from the backend -- and return it to the component, which automatically displays it once the data is available.
- The user performs an action that requires the component to display more data. For example, the user might scroll down a list of items in a `Grid` component.
- The component detects that more data is needed, and it passes a link:https://vaadin.com/api/platform/{moduleMavenVersion:com.vaadin:vaadin}/com/vaadin/flow/data/provider/Query.html[`Query`] object as a parameter to the callback methods. This object contains the necessary information about the data that should be displayed next to the user.
- The callback methods use this [classname]`Query` object to fetch only the required data -- usually from the backend -- and return it to the component, which automatically displays it once the data is available.

For example, to bind data lazily to a `Grid` you might do this:

Expand All @@ -150,12 +148,12 @@ grid.setItems(query -> // <1>
);
----
<1> To create a lazy binding, use an overloaded version of the [methodname]`setItems()` method that uses a callback instead of passing data directly to the component.
<2> Typically, you call the service layer from the callback, as is done here.
<2> Typically, you'd call the service layer from the callback, as is done here.
<3> The link:https://vaadin.com/api/platform/{moduleMavenVersion:com.vaadin:vaadin}/com/vaadin/flow/data/provider/Query.html#getOffset()[_offset_] refers to the first index of the item to fetch.
<4> The link:https://vaadin.com/api/platform/{moduleMavenVersion:com.vaadin:vaadin}/com/vaadin/flow/data/provider/Query.html#getLimit()[_limit_] refers to the number of items to fetch. When fetching more data, you should utilize [classname]`Query` properties to limit the amount of data to fetch.
<5> In this example, it's assumed that the backend returns a [classname]`List`. Therefore, you need to convert it to a [classname]`Stream`.
<5> In this example, it's assumed that the backend returns a [classname]`List`. Therefore, you'll need to convert it to a [classname]`Stream`.

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 a paged manner, so it's also possible to bind to "paging backends", such as Spring Data-based solutions.
The example above works well with JDBC backends, where you can request a set of rows from a given index. Vaadin Flow executes your data binding call in a 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` you would do something like this:

Expand All @@ -171,7 +169,7 @@ grid.setItems(query -> {

<1> Call a Spring Data repository to get 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.
<3> The query object also contains the page size.
<4> Return a stream of items from the Spring Data [classname]`Page` object.


Expand Down Expand Up @@ -211,11 +209,11 @@ public void bindWithSorting() {
----
<1> If you're using property-name-based column definition, `Grid` columns can be made sortable by their property names. The [methodname]`setSortableColumns()` method makes columns with given identifiers sortable and all others non-sortable.
<2> Alternatively, define a key to your columns, which is 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 backend understands. This example uses Spring Data based backend, so it is mostly converting Vaadin's QuerySortOrder hints to Spring's [classname]`Order` objects and finally passing the sort and paging details to the backend.
<3> 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's mostly converting Vaadin's QuerySortOrder hints to Spring's [classname]`Order` objects and finally passing the sort and paging details to the backend.

.Helpers for Spring Data based backends
.Spring Data Based Backend Helpers
[NOTE]
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're using Spring Data based backends, the above code examples can be written with one-liners using the helper methods in [classname]`VaadinSpringDataHelpers` class. It contains [methodname]`toSpringPageRequest()` and [methodname]`toSpringDataSort()` methods to convert automatically Vaadin specific query hints to their Spring Data relatives. Using the [methodname]`fromPagingRepository()` method, you can create a lazy sortable data binding directly to your repository.
The examples above are written for Spring Data based examples, but in a verbose way to keep them relevant for any kind of Java backend service. If you're using Spring Data based backends, the above code examples can be written with one-liners using the helper methods in [classname]`VaadinSpringDataHelpers` class. It contains [methodname]`toSpringPageRequest()` and [methodname]`toSpringDataSort()` methods to convert automatically Vaadin specific query hints to their Spring Data relatives. Using the [methodname]`fromPagingRepository()` method, you can create a lazy sortable data binding directly to your repository.


=== Filtering with Lazy Data Binding
Expand Down Expand Up @@ -243,12 +241,12 @@ private void listPersonsFilteredByName(String filterString) {

<1> 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.
<2> When a value-change event occurs, you should reset the data binding to use the new filter.
<3> The example backend uses SQL behind the scenes, so the filter string is wrapped in `%` characters to match anywhere in the text.
<3> The example backend uses SQL behind the scenes, so the filter string is wrapped with the `%` wildcard character to match anywhere in the text.
<4> 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.
The `ComboBox` examples below use the new data API available since Vaadin Flow 18, where the item count query isn't needed to fetch items.

You can handle filterable lazy data binding to a Spring Data repository as follows:

Expand Down Expand Up @@ -300,7 +298,7 @@ cb.setItemsWithFilterConverter(

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 [classname]`DataView` instance, which is returned by the [methodname]`setItems()` method. For example, to configure the estimate of rows and how the "virtual row count" is adjusted when the user scrolls down you could do this:
The usability can be improved by providing an estimate of the actual number of items in the binding code. The adjustment happens through a [classname]`DataView` instance, which is returned by the [methodname]`setItems()` method. For example, to configure the estimate of rows and how the "virtual row count" is adjusted when the user scrolls down, you could do this:

[source,java]
----
Expand All @@ -326,9 +324,45 @@ dataView.setItemCountCallback(q -> getPersonService().getPersonCount());
----


== Accessing Currently Shown Items
=== Callback for an Item Index

When using lazy data binding, the component can't know the index of the item in the data set, if it's not loaded yet. Index is needed, for example, when you want to scroll to an item's position in the component. [methodname]`setItemIndexProvider(ItemIndexProvider)` method in [classname]`LazyDataView` is used to provide a callback to get the index of the item in the data set.

The example below sets the item index provider that uses a service which uses the Spring Data repository. It fetches all persons and finds the index of the matching item in the list. This is not an optimal solution with a large data set, but it shows how to implement the callback. Callback should always ensure the data set used to find the item index matches the component's data set with the same sorting and filtering:

[source,java]
----
dataView.setItemIndexProvider((item, query) -> {
if(item == null) {
return null;
}
AtomicInteger index = new AtomicInteger();
return getPersonService().list(
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
.stream()
.peek(v -> index.incrementAndGet())
.anyMatch(item::equals) ?
index.get() - 1 : null;
});
----

The callback gives parameters of the target item, and the [classname]`Query` object to fetch the index. The query is prepared for fetching all items, including filter and sorting. The returned index is the index of the item in the filtered and sorted data set. If the item is not found, null is expected as a return value.

The index is inconsistent if the data set for the returned index is different from the component's data set. Changing the data set of either side during this call may cause an inconsistent index.

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 [methodname]`getGenericDataView()` method.
The index of an item is retrieved with [methodname]`getItemIndex(Object)` method in [classname]`DataView`. It works with lazy data binding only when the item index provider is set. Otherwise, it throws [classname]`UnsupportedOperationException`.

This is an example of a call that scrolls to the item's position in the component:

[source,java]
----
grid.scrollToIndex(dataView.getItemIndex(item));
----


== Accessing Displayed 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 purposes, the supertype of data views can be accessed with the [methodname]`getGenericDataView()` method.

[CAUTION]
Calling certain methods in data views can be an expensive operation. Particularly with lazy data binding, calling [methodname]`grid.getGenericDataView().getItems()` causes the whole data set to be loaded from the backend.
Expand All @@ -353,8 +387,7 @@ private void exportToCsvFile(Grid<Person> grid)
}
----

If you've 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 [methodname]`setItems()` method or through the [methodname]`getListDataView()` method. It's then possible to get the next or previous item from a certain item. Of course, this can be done by saving the original data structure,
but that way you can implement a generic UI logic without dependencies on the assigned data.
If you've 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 [methodname]`setItems()` method or through the [methodname]`getListDataView()` method. It's then possible to get the next or previous item from a certain item. Of course, this can be done by saving the original data structure, but that 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.

Expand All @@ -375,7 +408,7 @@ Button selectNext = new Button("Next", e -> {

== 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.
A typical scenario in Vaadin Flow applications is that data displayed, for example, in a `Grid` component, is edited elsewhere in the application. Editing an item elsewhere doesn't automatically update the UI in a listing component.

An easy way to refresh the component's content is to call [methodname]`setItems()` again with the fresh data. Alternatively, you can use finer-grained APIs in the `DataView` to update a portion of the dataset.

Expand Down Expand Up @@ -522,7 +555,7 @@ public static void listItems(Grid<Person> grid, PersonRepository repository) {
}
----

You can create a separate data provider class. The following example uses only the [classname]`FetchCallBack`, but you can also implement a full data provider by, for example, extending [classname]`AbstractbackendDataProvider`.
You can create a separate data provider class. The following example uses only the [classname]`FetchCallBack`, but you can also implement a full data provider by extending [classname]`AbstractbackendDataProvider`.

[source,java]
----
Expand All @@ -543,8 +576,9 @@ public class PersonDataProvider implements CallbackDataProvider.FetchCallback<Pe
personGrid.setItems(dataProvider);
----


[[data-binding.data-provider.item-identifiers]]
== Ensuring Item Identities are Stable and Unique
== Stable & Unique Item Identities

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.

Expand Down

0 comments on commit ffa5e2d

Please sign in to comment.