From vaadin-claude
Guides usage of Vaadin 25 data providers for Grid and ComboBox, covering in-memory and lazy loading, filtering, sorting, and Spring Pageable integration.
npx claudepluginhub vaadin/claude-plugin --plugin vaadin-claudeThis skill uses the workspace's default tool permissions.
Use the Vaadin MCP tools (`search_vaadin_docs`, `get_component_java_api`) to look up the latest documentation whenever uncertain about a specific API detail. Always set `vaadin_version` to `"25"` and `ui_language` to `"java"`.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Use the Vaadin MCP tools (search_vaadin_docs, get_component_java_api) to look up the latest documentation whenever uncertain about a specific API detail. Always set vaadin_version to "25" and ui_language to "java".
List<Person> people = personService.findAll();
grid.setItems(people);
Use when: the full dataset is small (hundreds to low thousands of items), or you've already loaded it for other reasons.
In-memory data supports client-side-like sorting and filtering through the ListDataView:
GridListDataView<Person> dataView = grid.setItems(people);
dataView.setFilter(p -> p.getAge() < 30);
dataView.setSortOrder(Person::getName, SortDirection.ASCENDING);
Only the visible portion of data is loaded. Filtering and sorting are delegated to the backend (typically a database).
With Spring (preferred — uses Pageable):
grid.setItemsPageable(personService::list);
// Service method signature:
public List<Person> list(Pageable pageable) {
return repository.findAllBy(pageable).getContent();
}
setItemsPageable converts Grid's internal query into a Spring Pageable with offset, limit, and sort information. This integrates directly with Spring Data repositories.
Use
Sliceinstead ofPage: The repository method should return aSlice, not aPage. APageexecutes an additionalCOUNTquery that Grid cannot use — Grid treats counting as a separate concern via a dedicated count callback. Use aSlice-returning repository method (e.g.,Slice<Person> findAllBy(Pageable pageable)) and provide a separate count method when needed.
Without Spring (generic callbacks):
grid.setItems(query ->
personService.fetch(query.getOffset(), query.getLimit()).stream()
);
The callback receives a Query object with getOffset(), getLimit(), and getSortOrders().
GridListDataView<Person> dataView = grid.setItems(allPeople);
filterField.addValueChangeListener(e ->
dataView.setFilter(person ->
person.getName().toLowerCase().contains(e.getValue().toLowerCase()))
);
Pass the filter value into your service callback:
// With Spring
GridLazyDataView<Person> dataView = grid.setItemsPageable(
pageable -> personService.list(pageable, filterField.getValue())
);
filterField.setValueChangeMode(ValueChangeMode.LAZY);
filterField.addValueChangeListener(e -> dataView.refreshAll());
// Without Spring
grid.setItems(query ->
personService.fetch(
query.getOffset(),
query.getLimit(),
query.getSortOrders(),
filterField.getValue()
).stream()
);
filterField.addValueChangeListener(e -> grid.getDataProvider().refreshAll());
Use ValueChangeMode.LAZY on the filter field — it waits for the user to pause typing before triggering, reducing backend calls.
Columns with Comparable types are automatically sortable. Customize with a Comparator:
grid.addColumn(Person::getName)
.setHeader("Name")
.setComparator(Comparator.comparing(p -> p.getName().toLowerCase()));
Declare which columns are sortable:
grid.setSortableColumns("name", "email"); // by property name
// or per-column:
grid.addColumn(Person::getTitle).setKey("title").setSortable(true);
Then handle query.getSortOrders() in your callback. With setItemsPageable, sorting is handled automatically via the Pageable object.
ComboBox supports lazy loading with built-in text filtering:
// With Spring
comboBox.setItemsPageable(productService::list);
// Service must accept filter string:
public List<Product> list(Pageable pageable, String filterString) { ... }
// Without Spring
comboBox.setItems(query ->
personService.fetch(
query.getOffset(),
query.getLimit(),
query.getFilter().orElse("")
).stream()
);
Without a count callback, Grid uses infinite scrolling (the scrollbar updates as more items are loaded). For a better UX, provide the count:
// With Spring
grid.setItemsPageable(personService::list, personService::count);
// Without Spring
grid.setItems(
query -> personService.fetch(query.getOffset(), query.getLimit()).stream(),
query -> personService.count()
);
If exact count is expensive, provide an estimate:
GridLazyDataView<Person> dataView = grid.setItems(fetchCallback);
dataView.setItemCountEstimate(1000);
dataView.setItemCountEstimateIncrease(500);
person.setEmail("new@example.com");
dataView.refreshItem(person); // updates just this row
dataView.refreshAll(); // re-fetches everything
GridListDataView<Person> dataView = grid.setItems(mutableList);
dataView.addItem(newPerson);
dataView.removeItem(oldPerson);
Components rely on hashCode() and equals() for item identity. These must be based on stable, unique properties (typically the database ID), not mutable fields:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(id, person.id); // stable identifier only
}
@Override
public int hashCode() {
return Objects.hash(id);
}
With Lombok: use @EqualsAndHashCode(onlyExplicitlyIncluded = true) and @EqualsAndHashCode.Include on the ID field.
The ID field must be final (e.g., private final Long id;) so that the hash code never changes after construction. A mutable ID violates the hashCode() contract and breaks Grid's internal identity tracking.
Do NOT Spring-manage Grids or DataProviders. They hold UI state and are tied to a single user session. Create them as plain classes, pass dependencies via constructors.
public class PersonGrid extends Grid<Person> {
public PersonGrid(PersonRepository repo) {
super(Person.class);
setItemsPageable(pageable -> repo.findAllBy(pageable).getContent());
setColumns("name", "email");
}
}
// In the view:
@Route("people")
@PermitAll
public class PeopleView extends VerticalLayout {
public PeopleView(PersonRepository repo) {
add(new PersonGrid(repo));
}
}
public class PersonDataProvider
extends AbstractBackEndDataProvider<Person, String> {
private final PersonRepository repo;
public PersonDataProvider(PersonRepository repo) {
this.repo = repo;
}
@Override
protected Stream<Person> fetchFromBackEnd(Query<Person, String> query) {
String filter = query.getFilter().orElse("");
return repo.findByNameContaining(filter,
VaadinSpringDataHelpers.toSpringPageRequest(query)).stream();
}
@Override
protected int sizeInBackEnd(Query<Person, String> query) {
String filter = query.getFilter().orElse("");
return (int) repo.countByNameContaining(filter);
}
}
setItemsPageable with Spring — it handles offset/limit/sort conversion to Pageable automatically, integrating cleanly with Spring Data.hashCode/equals — based on the entity's ID, not mutable fields. This is required for Grid to correctly track selections and updates."name" used in setKey(), setSortableColumns(), and sort-order handling are prone to typos. Define constants (e.g., public static final String COL_NAME = "name";) on the entity class and reference them in the view. This makes refactoring safer and enables compile-time checks.ValueChangeMode.LAZY on filter fields — prevents excessive backend queries while the user is typing.refreshAll() when filter/sort criteria change externally — the Grid doesn't automatically know when your filter field value changes.getGenericDataView().getItems() on lazy data — it loads the entire dataset into memory, defeating the purpose of lazy loading.