Grid
- Dynamic Height
- Sorting
- Filtering
- Lazy Loading
- Item Details
- Empty State
- Context Menu
- Tooltips
- Cell Focus
- Related Components
Vaadin Grid is a component for displaying tabular data, including various enhancements to grid renderings.
Some of the more complex features of this component are described on separate tabs:
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getFirstName).setHeader("First name");
grid.addColumn(Person::getLastName).setHeader("Last name");
grid.addColumn(Person::getEmail).setHeader("Email");
grid.addColumn(Person::getProfession).setHeader("Profession");
List<Person> people = DataService.getPeople();
grid.setItems(people);
Note
|
Auto-generated columns in Flow Grid
Although most code examples define columns explicitly, the Flow component can generated them automatically based root-level properties on the bean class if you pass it as an argument to the constructor, e.g. new Grid<>(Person.class);
|
Dynamic Height
Grid has a default height of 400 pixels. It becomes scrollable when items contained in it overflow the allocated space.
In addition to setting any fixed or relative value, the height of a grid can be set by the number of items in the dataset. The grid expands and retracts based on the row count. This feature disables scrolling. It shouldn’t be used with large data sets since it might cause performance issues.
Notice how the height of the rows in the earlier example adjusts because of the text in the Address cells wrapping. With that in mind, click the gray icon at the top right corner of the example below to open it in a new browser tab. Try resizing it, making it narrower and then wider. Notice how the rows are always the same height and that the text doesn’t wrap. Instead, the text is truncated with ellipses.
Sorting
Any column can be used for sorting the data displayed. Enable sorting to allow the user to sort items alphabetically, numerically, by date, or by some other method.
The arrowhead symbols in the column header indicate the current sorting direction. When toggled, the direction will cycle between ascending, descending and none.
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getId).setHeader("Id").setSortable(true);
grid.addColumn(Person::getFullName).setHeader("Name").setSortable(true);
grid.addColumn(Person::getEmail).setHeader("Email").setSortable(true);
grid.addColumn(Person::getProfession).setHeader("Profession")
.setSortable(true);
grid.addColumn(new LocalDateRenderer<>(GridSorting::getPersonBirthday,
"yyyy-MM-dd")).setHeader("Birthday").setSortable(true)
.setComparator(Person::getBirthday);
Sorting by Multiple Columns
Multi-sort mode allows the Grid to be sorted by multiple columns simultaneously.
In normal multi-sort mode, additional sorting columns are applied simply by clicking their headers.
A separate multi-sort on shift-click mode combines single and multi-column sorting by adding more sorting columns only when the user holds the Shift key while clicking their headers.
The order in which multi-sort columns (known as sorting criteria) are evaluated is determined by the multi-sort priority setting.
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getId).setHeader("Id").setSortable(true);
grid.addColumn(Person::getFullName).setHeader("Name").setSortable(true);
grid.addColumn(Person::getEmail).setHeader("Email").setSortable(true);
grid.addColumn(Person::getProfession).setHeader("Profession")
.setSortable(true);
grid.addColumn(new LocalDateRenderer<>(GridMultiSort::getPersonBirthday,
"yyyy-MM-dd")).setHeader("Birthday").setSortable(true)
.setComparator(Person::getBirthday);
grid.setMultiSort(true, MultiSortPriority.APPEND);
Note
|
Shift-Click Multi-Sorting Accessibility Issues
The multi-sort on shift-click mode is not recommended for applications for which accessibility is important. This feature is unlikely to work well with assistive technologies, and the lack of visual affordance makes it difficult to discover for sighted users.
|
Specifying Sort Property
Columns with rich or custom content can be sorted by defining the property by which to sort. For example, in the table here there’s a column containing the employees' first and last names, avatar images, and email addresses. By clicking on the heading for that column, it’ll sort the data by their last names.
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(createEmployeeRenderer()).setHeader("Employee")
.setAutoWidth(true).setFlexGrow(0)
.setComparator(Person::getLastName);
grid.addColumn(createBirthdayRenderer()).setHeader("Birthdate")
.setComparator(Person::getBirthday);
Sorting helps users find and examine data. Therefore, it’s recommended to enable sorting for all applicable columns. An exception, though, would be when the order is an essential part of the data itself, such as with prioritized lists.
Filtering
Filtering allows the user to find a specific item or subset of items. You can add filters to Grid columns or use external filter fields.
For instance, try typing anna
in the input box for Name below. When you’re finished, the data shown is only people who have anna in their name. That includes some with the names Anna and Annabelle, as well as some with Arianna and Brianna.
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
Grid.Column<Person> nameColumn = grid.addColumn(createPersonRenderer())
.setWidth("230px").setFlexGrow(0);
Grid.Column<Person> emailColumn = grid.addColumn(Person::getEmail);
Grid.Column<Person> professionColumn = grid
.addColumn(Person::getProfession);
List<Person> people = DataService.getPeople();
GridListDataView<Person> dataView = grid.setItems(people);
PersonFilter personFilter = new PersonFilter(dataView);
grid.getHeaderRows().clear();
HeaderRow headerRow = grid.appendHeaderRow();
headerRow.getCell(nameColumn).setComponent(
createFilterHeader("Name", personFilter::setFullName));
headerRow.getCell(emailColumn).setComponent(
createFilterHeader("Email", personFilter::setEmail));
headerRow.getCell(professionColumn).setComponent(
createFilterHeader("Profession", personFilter::setProfession));
...
private static Component createFilterHeader(String labelText,
Consumer<String> filterChangeConsumer) {
NativeLabel label = new NativeLabel(labelText);
label.getStyle().set("padding-top", "var(--lumo-space-m)")
.set("font-size", "var(--lumo-font-size-xs)");
TextField textField = new TextField();
textField.setValueChangeMode(ValueChangeMode.EAGER);
textField.setClearButtonVisible(true);
textField.addThemeVariants(TextFieldVariant.LUMO_SMALL);
textField.setWidthFull();
textField.getStyle().set("max-width", "100%");
textField.addValueChangeListener(
e -> filterChangeConsumer.accept(e.getValue()));
VerticalLayout layout = new VerticalLayout(label, textField);
layout.getThemeList().clear();
layout.getThemeList().add("spacing-xs");
return layout;
}
private static class PersonFilter {
private final GridListDataView<Person> dataView;
private String fullName;
private String email;
private String profession;
public PersonFilter(GridListDataView<Person> dataView) {
this.dataView = dataView;
this.dataView.addFilter(this::test);
}
public void setFullName(String fullName) {
this.fullName = fullName;
this.dataView.refreshAll();
}
public void setEmail(String email) {
this.email = email;
this.dataView.refreshAll();
}
public void setProfession(String profession) {
this.profession = profession;
this.dataView.refreshAll();
}
public boolean test(Person person) {
boolean matchesFullName = matches(person.getFullName(), fullName);
boolean matchesEmail = matches(person.getEmail(), email);
boolean matchesProfession = matches(person.getProfession(),
profession);
return matchesFullName && matchesEmail && matchesProfession;
}
private boolean matches(String value, String searchTerm) {
return searchTerm == null || searchTerm.isEmpty()
|| value.toLowerCase().contains(searchTerm.toLowerCase());
}
}
Place filters outside the grid when the filter is based on multiple columns, or when a bigger field or more complex filter UI is needed, one which wouldn’t fit well in a column. In the example here, whatever you type in the search box can be matched against all of the columns. Type Rheumatologist
in the search box. The results show only the rows with that profession.
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(createPersonRenderer()).setHeader("Name").setFlexGrow(0)
.setWidth("230px");
grid.addColumn(Person::getEmail).setHeader("Email");
grid.addColumn(Person::getProfession).setHeader("Profession");
List<Person> people = DataService.getPeople();
GridListDataView<Person> dataView = grid.setItems(people);
TextField searchField = new TextField();
searchField.setWidth("50%");
searchField.setPlaceholder("Search");
searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH));
searchField.setValueChangeMode(ValueChangeMode.EAGER);
searchField.addValueChangeListener(e -> dataView.refreshAll());
dataView.addFilter(person -> {
String searchTerm = searchField.getValue().trim();
if (searchTerm.isEmpty())
return true;
boolean matchesFullName = matchesTerm(person.getFullName(),
searchTerm);
boolean matchesEmail = matchesTerm(person.getEmail(), searchTerm);
boolean matchesProfession = matchesTerm(person.getProfession(),
searchTerm);
return matchesFullName || matchesEmail || matchesProfession;
});
Lazy Loading
When you want to display a list of items that would be quite large to load entirely into memory, or you want to load items from a database, data providers can be used to provide lazy loading through pagination.
The following example works like the earlier example, but it uses a data provider for lazy loading, sorting, and filtering items.
new tab
private PersonFilter personFilter = new PersonFilter();
private PersonDataProvider dataProvider = new PersonDataProvider();
private ConfigurableFilterDataProvider<Person, Void, PersonFilter> filterDataProvider = dataProvider
.withConfigurableFilter();
public GridDataProvider() {
Grid<Person> grid = new Grid<>();
grid.addColumn(Person::getFullName, "name").setHeader("Name");
grid.addColumn(Person::getProfession, "profession")
.setHeader("Profession");
grid.setItems(filterDataProvider);
TextField searchField = new TextField();
searchField.setWidth("50%");
searchField.setPlaceholder("Search");
searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH));
searchField.setValueChangeMode(ValueChangeMode.EAGER);
searchField.addValueChangeListener(e -> {
personFilter.setSearchTerm(e.getValue());
filterDataProvider.setFilter(personFilter);
});
VerticalLayout layout = new VerticalLayout(searchField, grid);
layout.setPadding(false);
add(layout);
}
To learn more about data providers in Flow, see the Binding Items to Components documentation page.
Lazy Column Rendering
Grids containing a large number of columns can sometimes exhibit performance issues. If many of the columns are typically outside the visible viewport, rendering performance can be optimized by using "lazy column rendering" mode.
This mode enables virtual scrolling horizontally. It renders body cells only when their corresponding columns are inside the visible viewport.
Lazy rendering should be used only with a large number of columns and performance is a high priority. For most use cases, though, the default "eager" mode is recommended.
When considering whether to use the "lazy" mode, keep the following factors in mind:
- Row Height
-
When only a number of columns are visible at once, the height of a row can only be that of the highest cell currently visible on that row. Make sure each cell on a single row has the same height as all of the other cells on the row. Otherwise, users may notice jumpiness when horizontally scrolling the grid as lazily rendered cells with different heights are scrolled into view.
- Auto-Width Columns
-
For columns that are initially outside the visible viewport, but still use auto-width, only the header content is taken into account when calculating the column width. This is because the body cells of the columns outside the viewport are not rendered initially.
- Screen Reader Compatibility
-
Screen readers may not be able to associate the focused cells with the correct headers when only a subset of the body cells on a row is rendered.
- Keyboard Navigation
-
Tabbing through focusable elements inside the grid body may not work as expected. This is because some of the columns that would include focusable elements in the body cells may be outside the visible viewport and thus not rendered.
- No Improvement If All Columns Visible
-
The lazy column rendering mode can only improve the rendering performance when a significant portion of the columns are outside of the Grid’s visible viewport. It has no effect on Grids in which all columns are visible without horizontal scrolling.
Item Details
Item details are expandable content areas that can be displayed below the regular content of a row. They can be used to display more information about an item. By default, an item’s details are toggled by clicking on the item’s row. Try clicking on one of the rows in the example here. Notice that when you do, the row is expanded to show the person’s email address, telephone number, and home address. If you click on the row again, it’s collapsed back to a single line.
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getFullName).setHeader("Name");
grid.addColumn(Person::getProfession).setHeader("Profession");
grid.setItemDetailsRenderer(createPersonDetailsRenderer());
...
private static ComponentRenderer<PersonDetailsFormLayout, Person> createPersonDetailsRenderer() {
return new ComponentRenderer<>(PersonDetailsFormLayout::new,
PersonDetailsFormLayout::setPerson);
}
private static class PersonDetailsFormLayout extends FormLayout {
private final TextField emailField = new TextField("Email address");
private final TextField phoneField = new TextField("Phone number");
private final TextField streetField = new TextField("Street address");
private final TextField zipField = new TextField("ZIP code");
private final TextField cityField = new TextField("City");
private final TextField stateField = new TextField("State");
public PersonDetailsFormLayout() {
Stream.of(emailField, phoneField, streetField, zipField, cityField,
stateField).forEach(field -> {
field.setReadOnly(true);
add(field);
});
setResponsiveSteps(new ResponsiveStep("0", 3));
setColspan(emailField, 3);
setColspan(phoneField, 3);
setColspan(streetField, 3);
}
public void setPerson(Person person) {
emailField.setValue(person.getEmail());
phoneField.setValue(person.getAddress().getPhone());
streetField.setValue(person.getAddress().getStreet());
zipField.setValue(person.getAddress().getZip());
cityField.setValue(person.getAddress().getCity());
stateField.setValue(person.getAddress().getState());
}
}
The default toggle behavior can be replaced by programmatically toggling the details visibility, such as from a button click.
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(createToggleDetailsRenderer(grid)).setWidth("80px")
.setFlexGrow(0).setFrozen(true);
grid.addColumn(Person::getFullName).setHeader("Name");
grid.addColumn(Person::getProfession).setHeader("Profession");
grid.setDetailsVisibleOnClick(false);
grid.setItemDetailsRenderer(createPersonDetailsRenderer());
...
private static Renderer<Person> createToggleDetailsRenderer(
Grid<Person> grid) {
return LitRenderer
.<Person> of("""
<vaadin-button
theme="tertiary icon"
aria-label="Toggle details"
aria-expanded="${model.detailsOpened ? 'true' : 'false'}"
@click="${handleClick}"
>
<vaadin-icon
.icon="${model.detailsOpened ? 'lumo:angle-down' : 'lumo:angle-right'}"
></vaadin-icon>
</vaadin-button>
""")
.withFunction("handleClick",
person -> grid.setDetailsVisible(person,
!grid.isDetailsVisible(person)));
}
Tooltips can be used as a lightweight alternative to the item details panel.
Empty State
When there’s no data available for the grid to display any rows, the space between the header and footer is left blank by default. You can use the empty state feature to provide a message or other content in this area to inform the user that there are no items to show.
Context Menu
You can use Context Menu to provide shortcuts for the user. It appears on a right-click by default. In a mobile browser, a long press opens the menu. In the example here, try right-clicking on one of the rows. You’ll notice a box appears with a list of choices: Edit the row, delete it, email the person, or call them. If this example were fully configured, the latter two would open the related application (i.e., the default email program or a telephone application).
Using a context menu shouldn’t be the only way of accomplishing a task, though. The same functionality needs to be accessible elsewhere in the UI. See the documentation page on Context Menu for more information.
new tab
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getFirstName).setHeader("First name");
grid.addColumn(Person::getLastName).setHeader("Last name");
grid.addColumn(Person::getEmail).setHeader("Email");
grid.addColumn(Person::getProfession).setHeader("Profession");
PersonContextMenu contextMenu = new PersonContextMenu(grid);
add(grid);
...
private static class PersonContextMenu extends GridContextMenu<Person> {
public PersonContextMenu(Grid<Person> target) {
super(target);
addItem("Edit", e -> e.getItem().ifPresent(person -> {
// System.out.printf("Edit: %s%n", person.getFullName());
}));
addItem("Delete", e -> e.getItem().ifPresent(person -> {
// System.out.printf("Delete: %s%n", person.getFullName());
}));
add(new Hr());
GridMenuItem<Person> emailItem = addItem("Email",
e -> e.getItem().ifPresent(person -> {
// System.out.printf("Email: %s%n",
// person.getFullName());
}));
GridMenuItem<Person> phoneItem = addItem("Call",
e -> e.getItem().ifPresent(person -> {
// System.out.printf("Phone: %s%n",
// person.getFullName());
}));
setDynamicContentHandler(person -> {
// Do not show context menu when header is clicked
if (person == null)
return false;
emailItem
.setText(String.format("Email: %s", person.getEmail()));
phoneItem.setText(String.format("Call: %s",
person.getAddress().getPhone()));
return true;
});
}
}
Tooltips
Tooltips on cells can be useful in many situations: They can be used to give more details on the contents of a cell — if an item details panel would be overkill or otherwise undesirable. They can show the full text of a cell if it’s too long to fit feasibly into the cell itself — if wrapping the cell contents is insufficient or otherwise undesirable. Or they can give textual explanations for non-text content, such as status icons.
In the example here, hold your mouse pointer over the birthday date for one of the rows. A tooltip should appear indicating the age of the person. Now hover over one of the status icons, an X or a checkmark. It’ll use Tooltips to interpret the meaning of the icons.
new tab
grid.addColumn(person -> getFormattedPersonBirthday(person))
.setTooltipGenerator(person -> "Age: " + getPersonAge(person))
.setHeader("Birthday");
grid.addComponentColumn(person -> createStatusIcon(person.getStatus()))
.setTooltipGenerator(person -> person.getStatus())
.setHeader("Status");
See the Tooltips documentation page for details on tooltip configuration.
Cell Focus
Many of the explanations and examples above alluded to giving the focus to rows and cells. Cells can be focused by clicking on a cell, or with the keyboard. The following keyboard shortcuts are available with Grid:
Keys | Action |
---|---|
Tab | Switches focus between sections of the grid (i.e., header, body, footer). |
Left, Up, Right, and Down Arrow Keys | Moves focus between cells within a section of the grid. |
Page Up | Moves cell focus up by one page of visible rows. |
Page Down | Moves cell focus down by one page of visible rows. |
Home | Moves focus to the first cell in a row. |
End | Moves focus to the last cell in a row. |
The cell focus event can be used to be notified when the user changes focus between cells. By default, the focus outline is only visible when using keyboard navigation. For illustrative purposes, the example below also uses custom styles to show the focus outline when clicking on cells. Try clicking on a cell. Notice how the cell is highlighted and notice the information shown at the bottom, the information provided about the event.
new tab
grid.addCellFocusListener(event -> {
CellFocusEvent.GridSection section = event.getSection();
String column = event.getColumn().map(Grid.Column::getKey)
.orElse("Not available");
String row = event.getItem()
.map(value -> String.valueOf(people.indexOf(value)))
.orElse("Not available");
String fullName = event.getItem().map(Person::getFullName)
.orElse("Not available");
String eventSummary = String.format(
"Section: %s%nRow: %s%nColumn: %s%nPerson: %s", section,
row, column, fullName);
textArea.setValue(eventSummary);
});
Related Components
Component | Usage Recommendation |
---|---|
Component for creating, displaying, updating, and deleting tabular data. | |
Component for showing and editing tabular data. | |
Component for showing hierarchical tabular data. | |
Lightweight component for lightweight, single-column lists. |
AC63AABF-4102-4C3E-9776-A09DDC04EF37