Skip to content

Commit 092dde6

Browse files
authored
Use setItemsPageable in data binding docs (#4148)
1 parent 269ace1 commit 092dde6

File tree

2 files changed

+110
-89
lines changed

2 files changed

+110
-89
lines changed

.github/styles/config/vocabularies/Docs/accept.txt

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ OAuth
133133
OSGi
134134
Okta
135135
OpenAPI
136+
pageable
136137
Payara
137138
performant
138139
[pP]ersister

articles/flow/binding-data/data-provider.adoc

+109-89
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,36 @@ Currently, only `Grid` and `ComboBox` support lazy data binding. To use it, thou
134134

135135
- 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.
136136
- 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.
137-
- 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 displays it once the data is available.
137+
- The callback methods use this [classname]`Query` object (for generic callbacks) or a [classname]`Pageable` object (for Spring services) to fetch only the required data and return it to the component, which displays it once the data is available.
138138

139-
For example, to bind data lazily to a `Grid` you might do this:
139+
140+
=== Binding a Grid to a Spring Service for Lazy Loading
141+
142+
For example, to bind a `Grid` to load its data on demand from a Spring service, you do:
143+
144+
[source,java]
145+
----
146+
grid.setItemsPageable(productService::list);
147+
148+
// Assuming a service like
149+
class ProductService {
150+
public List<Product> list(Pageable pageable) { ... }
151+
}
152+
----
153+
154+
The `setItemsPageable` method takes care of making the Grid request available in a Spring friendly format, as a `Pageable` instance. This `Pageable` instance contains information about which page (offset/limit) to load and also about how to sort the data (based on what is selected in the Grid). Many spring data sources use `Pageable` so you can directly pass it into e.g. a JPA repository class.
155+
156+
If your Spring service requires additional parameters, you can use the longer version
157+
158+
[source,java]
159+
----
160+
grid.setItemsPageable(pageable -> productService.list(pageable, somethingElse));
161+
----
162+
163+
164+
=== Lazy Data Binding When not Using Spring
165+
166+
For the generic case, you use the `setItems` method which uses a more generic `Query` type parameter. The `Query` parameter contains the same information as `Pageable` for Spring, i.e. which rows to fetch (offset/limit) and which sort order to use.
140167

141168
[source,java]
142169
----
@@ -155,127 +182,103 @@ grid.setItems(query -> // <1>
155182
<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.
156183
<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`.
157184

158-
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.
185+
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".
186+
187+
[[data-binding.data-provider.lazy-sorting]]
188+
189+
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 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.
159190

160-
For example, to do lazy data binding from a Spring Data Repository to `Grid` you would do something like this:
191+
To make sorting work in a lazy data binding, you need to pass the hints that `Grid` provides in the [classname]`Query` object to your backend logic. For example, to enable sortable lazy data binding to a custom service like
161192

162193
[source,java]
163194
----
164-
grid.setItems(query -> {
165-
return repository.findAll( // <1>
166-
PageRequest.of(query.getPage(), // <2>
167-
query.getPageSize()) // <3>
168-
).stream(); // <4>
169-
});
195+
public List<Person> listPersons(int page, int offset, String sortProperty, boolean ascending) { ... }
170196
----
171197

172-
<1> Call a Spring Data repository to get the requested result set.
173-
<2> The query object contains a shorthand for a zero-based page index.
174-
<3> The query object also contains the page size.
175-
<4> Return a stream of items from the Spring Data [classname]`Page` object.
176-
177-
178-
[[data-binding.data-provider.lazy-sorting]]
179-
=== Sorting with Lazy Data Binding
180-
181-
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 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.
182-
183-
To make sorting work in a lazy data binding, you need to pass the hints that `Grid` provides in the [classname]`Query` object to your backend logic. For example, to enable sortable lazy data binding to a Spring Data repository, do this:
198+
you could do
184199

185200
[source,java]
186201
----
187-
public void bindWithSorting() {
188-
Grid<Person> grid = new Grid<>(Person.class);
189-
grid.setSortableColumns("name", "email"); // <1>
190-
grid.addColumn(person -> person.getTitle())
191-
.setHeader("Title")
192-
.setKey("title").setSortable(true); // <2>
193-
grid.setItems(query -> { // <3>
194-
var vaadinSortOrders = query.getSortOrders();
195-
var springSortOrders = new ArrayList<Sort.Order>();
196-
for (QuerySortOrder so : vaadinSortOrders) {
197-
String colKey = so.getSorted();
198-
if(so.getDirection() == SortDirection.ASCENDING) {
199-
springSortOrders.add(Sort.Order.asc(colKey));
200-
}
201-
}
202-
return repository.findAll(
203-
PageRequest.of(
204-
query.getPage(),
205-
query.getPageSize(),
206-
Sort.by(springSortOrders)
207-
)
208-
).stream();
209-
});
210-
}
202+
Grid<Person> grid = new Grid<>(Person.class);
203+
grid.setSortableColumns("name", "email"); // <1>
204+
grid.addColumn(person -> person.getTitle())
205+
.setHeader("Title")
206+
.setKey("title").setSortable(true); // <2>
207+
grid.setItems(query -> { // <3>
208+
String colKey = null;
209+
boolean ascending = true;
210+
if (!query.getSortOrders().isEmpty()) { // <4>
211+
QuerySortOrder firstSort = query.getSortOrders().get(0);
212+
colKey = firstSort.getSorted();
213+
ascending = firstSort.getDirection() == SortDirection.ASCENDING;
214+
}
215+
return personService.list(query.getPage(), query.getOffset(), colKey, ascending).stream();
216+
});
211217
----
212218
<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.
213219
<2> Alternatively, define a key to your columns, which is passed to the callback, and define the column to be sortable.
214220
<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.
215-
216-
.Spring Data Based Backend Helpers
217-
[NOTE]
218-
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.
221+
<4> For a real implementation, you should take into account that there can be many sort orders, i.e. "first sort by birth year and then by last name"
219222

220223

221224
=== Filtering with Lazy Data Binding
222225

223226
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.
224227

225-
As an example, to handle filterable lazy data binding to a Spring Data repository in `Grid`, you might do this:
228+
For example, suppose you have a Spring service that supports filtering like this:
226229

227230
[source,java]
228231
----
229-
public void initFiltering() {
230-
filterTextField.setValueChangeMode(ValueChangeMode.LAZY); // <1>
231-
filterTextField.addValueChangeListener(e -> listPersonsFilteredByName(e.getValue())); // <2>
232-
}
232+
public List<Product> list(Pageable pageable, String lastNameFilter) {
233+
return repository.findAllByLastName(lastNameFilter, pageable);
234+
}
235+
----
233236

234-
private void listPersonsFilteredByName(String filterString) {
235-
String likeFilter = "%" + filterString + "%";// <3>
236-
grid.setItems(q -> repo
237-
.findByNameLikeIgnoreCase(
238-
likeFilter, // <4>
239-
PageRequest.of(q.getPage(), q.getPageSize()))
240-
.stream());
241-
}
237+
You can then set up a filtering text field that causes the grid to refresh the data when the filter is changed, like so:
238+
239+
[source,java]
240+
----
241+
TextField filterTextField = new TextField("Filter using last name");
242+
filterTextField.setValueChangeMode(ValueChangeMode.LAZY); // <1>
243+
244+
GridLazyDataView<Product> dataView = grid.setItemsPageable(pageable -> productService.list(pageable, filterTextField.getValue(), somethingElse)); // <2>
245+
filterTextField.addValueChangeListener(e -> dataView.refreshAll()); // <3>
242246
----
247+
<1> The lazy data value change mode is optimal for filtering purposes. Queries to the backend are only done when a user makes a small pause while typing.
248+
<2> Passes the current filter value to the service.
249+
<3> When a value-change event occurs, asks the data provider to load new values.
243250

244-
<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.
245-
<2> When a value-change event occurs, you should reset the data binding to use the new filter.
246-
<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.
247-
<4> Pass the filter to your backend in the binding.
251+
If you are not using Spring, you can pass a filter value in the same way. You can also pass more complex filtering values like JPA specification instances or whatever is needed.
248252

249-
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.
250253

251-
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.
254+
=== Binding a `ComboBox` to a Spring Service for Lazy Loading
252255

253-
You can handle filterable lazy data binding to a Spring Data repository as follows:
256+
A Combo Box differs in its data binding for Grid in two ways: it doesn't have UI controls for defining the sort order; and it has a UI control for defining a filter string.
257+
258+
The API for connecting a Combo Box to a Spring Service is quite similar to the Grid API:
254259

255260
[source,java]
256261
----
257-
ComboBox<Person> cb = new ComboBox<>();
258-
cb.setItems(
259-
query -> repo.findByNameLikeIgnoreCase(
260-
// Add `%` marks to filter for an SQL "LIKE" query
261-
"%" + query.getFilter().orElse("") + "%",
262-
PageRequest.of(query.getPage(), query.getPageSize()))
263-
.stream()
264-
);
262+
combobox.setItemsPageable(productService::list);
263+
264+
// Assuming a service like
265+
class ProductService {
266+
public List<Product> list(Pageable pageable, String filterString) { ... }
267+
}
265268
----
266269

267-
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:
270+
A service used for a Combo Box always has at least two parameters: a `Pageable` instance to define which page to load; and a `String` that provides the input the user has typed into the `combobox` field -- which is empty by default.
271+
272+
Similarly, when not using Spring, you would have the following:
268273

269274
[source,java]
270275
----
271-
cb.setItems(
272-
query -> repo.findByNameLikeIgnoreCase(
273-
"%" + query.getFilter().orElse("") + "%",
274-
PageRequest.of(query.getPage(), query.getPageSize()))
275-
.stream(),
276-
query -> (int) repo.countByNameLikeIgnoreCase(
277-
"%" + query.getFilter().orElse("") + "%"));
276+
ComboBox<Person> comboBox = new ComboBox<>(Person.class);
277+
comboBox.setItems(query -> {
278+
return personService.list(query.getPage(), query.getOffset(), query.getFilter().orElse("")).stream(); // <1>
279+
});
278280
----
281+
<1> The [classname]`Query` object contains the filter
279282

280283
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 like so:
281284

@@ -292,15 +295,32 @@ cb.setItemsWithFilterConverter(
292295
textFilter -> textFilter.isEmpty() ? null // <2>
293296
: Integer.parseInt(textFilter));
294297
----
295-
<1> [classname]`Query` object contains the filter of type returned by given converter.
298+
<1> The [classname]`Query` object contains the filter of type returned by given converter.
296299
<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 backend.
297300

298301

299302
=== Improving Scrolling Behavior
300303

301-
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.
304+
With the lazy data binding described above, the component doesn't know how many items are actually available. When a user scrolls to the end of the scrollable area, the component (e.g. `Grid` or `ComboBox`) polls your callbacks for more items. If new items are found, these are added to the component. This pattern, often called infinite scrolling, causes the scrollbar to be updated when new items are added on the fly and does not allow the user to immediately scroll to the end.
305+
306+
The usability can be improved by either providing the exact number of items available or providing an estimate of the number of items.
307+
308+
If your service is able to provide the exact number of items available, you can add an additional "count" callback to `setItems` or `setItemsPageable` like this:
309+
310+
[source,java]
311+
----
312+
grid.setItemsPageable(productService::list, productService::count);
313+
314+
// Assuming a service like
315+
class ProductService {
316+
public List<Product> list(Pageable pageable) { ... }
317+
public long count(Pageable pageable) { ... } <1>
318+
}
319+
----
320+
<1> The count method should return the total number of items available. The _pageable_ instance is also passed to the count method but in most cases you do not need to take it into account.
302321

303-
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:
322+
If your service does not provide an exact count or requesting it is too costly (i.e. takes too long), you can provide an estimate instead.
323+
This you can do 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:
304324

305325
[source,java]
306326
----

0 commit comments

Comments
 (0)