Tree Grid
- Usage
- Styling
Tree Grid is a component for displaying hierarchical tabular data grouped into expandable nodes.
new tab
Source code
TreeGridBasic.java
package com.vaadin.demo.component.treegrid;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.treegrid.TreeGrid;
import com.vaadin.flow.router.Route;
import java.util.List;
@Route("tree-grid-basic")
public class TreeGridBasic extends Div {
private List<Person> managers = DataService.getManagers();
public TreeGridBasic() {
TreeGrid<Person> treeGrid = new TreeGrid<>();
treeGrid.setItems(managers, this::getStaff);
treeGrid.addHierarchyColumn(Person::getFirstName)
.setHeader("First name");
treeGrid.addColumn(Person::getLastName).setHeader("Last name");
treeGrid.addColumn(Person::getEmail).setHeader("Email");
add(treeGrid);
}
public List<Person> getStaff(Person manager) {
return DataService.getPeople(manager.getId());
}
}
Person.java
package com.vaadin.demo.domain;
import java.util.Date;
import javax.annotation.Nonnull;
public class Person {
@Nonnull
private String firstName;
@Nonnull
private String lastName;
@Nonnull
private String email;
@Nonnull
private Date birthday;
@Nonnull
private Integer id;
@Nonnull
private Boolean subscriber;
@Nonnull
private String membership;
@Nonnull
private String pictureUrl;
@Nonnull
private String profession;
@Nonnull
private Address address;
private Integer managerId;
@Nonnull
private Boolean manager;
@Nonnull
private String status;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFullName() {
return firstName + " " + lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public boolean isSubscriber() {
return subscriber;
}
public void setSubscriber(boolean subscriber) {
this.subscriber = subscriber;
}
public String getMembership() {
return membership;
}
public void setMembership(String membership) {
this.membership = membership;
}
public String getPictureUrl() {
return pictureUrl;
}
public void setPictureUrl(String pictureUrl) {
this.pictureUrl = pictureUrl;
}
public String getProfession() {
return profession;
}
public void setProfession(String profession) {
this.profession = profession;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
return id == other.id;
}
public Integer getManagerId() {
return managerId;
}
public void setManagerId(Integer managerId) {
this.managerId = managerId;
}
public boolean isManager() {
return manager;
}
public void setManager(boolean manager) {
this.manager = manager;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
tree-grid-basic.tsx
import React from 'react';
import {
Grid,
type GridDataProviderCallback,
type GridDataProviderParams,
} from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { GridTreeColumn } from '@vaadin/react-components/GridTreeColumn.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
async function dataProvider(
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) {
// The requested page and the full length of the corresponding
// hierarchy level is requested from the data service
const { people, hierarchyLevelSize } = await getPeople({
count: params.pageSize,
startIndex: params.page * params.pageSize,
managerId: params.parentItem ? params.parentItem.id : null,
});
callback(people, hierarchyLevelSize);
}
function Example() {
return (
<Grid itemHasChildrenPath="manager" dataProvider={dataProvider}>
<GridTreeColumn path="firstName" />
<GridColumn path="lastName" />
<GridColumn path="email" />
</Grid>
);
}
tree-grid-basic.ts
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-tree-column.js';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import type { GridDataProviderCallback, GridDataProviderParams } from '@vaadin/grid';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('tree-grid-basic')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
async dataProvider(
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) {
// The requested page and the full length of the corresponding
// hierarchy level is requested from the data service
const { people, hierarchyLevelSize } = await getPeople({
count: params.pageSize,
startIndex: params.page * params.pageSize,
managerId: params.parentItem ? params.parentItem.id : null,
});
callback(people, hierarchyLevelSize);
}
protected override render() {
return html`
<vaadin-grid .itemHasChildrenPath="${'manager'}" .dataProvider="${this.dataProvider}">
<vaadin-grid-tree-column path="firstName"></vaadin-grid-tree-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
</vaadin-grid>
`;
}
}
Note
| Tree Grid is an extension of the Grid component. Therefore, all of Grid’s features are available in Tree Grid. However, Tree Grid isn’t meant to be used as a navigation menu. |
Tree Column
The tree column contains the toggles for expanding and collapsing nodes. Nodes are opened and closed by clicking a tree column’s cell. They can also be toggled programmatically.
new tab
Source code
TreeGridColumn.java
package com.vaadin.demo.component.treegrid;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.treegrid.TreeGrid;
import com.vaadin.flow.router.Route;
import java.util.List;
@Route("tree-grid-column")
public class TreeGridColumn extends Div {
private List<Person> managers = DataService.getManagers();
public TreeGridColumn() {
TreeGrid<Person> treeGrid = new TreeGrid<>();
treeGrid.setItems(managers, this::getStaff);
treeGrid.addHierarchyColumn(Person::getFirstName)
.setHeader("First name");
treeGrid.addColumn(Person::getLastName).setHeader("Last name");
treeGrid.addColumn(Person::getEmail).setHeader("Email");
H3 employees = new H3("Employees");
employees.getStyle().set("margin", "0");
Button expand = new Button("Expand All");
expand.addClickListener(event -> treeGrid.expand(managers));
Button collapse = new Button("Collapse All");
collapse.addClickListener(event -> treeGrid.collapse(managers));
HorizontalLayout header = new HorizontalLayout(employees, expand,
collapse);
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setHeight("var(--lumo-space-xl)");
header.setFlexGrow(1, employees);
add(header, treeGrid);
}
public List<Person> getStaff(Person manager) {
return DataService.getPeople(manager.getId());
}
}
Person.java
package com.vaadin.demo.domain;
import java.util.Date;
import javax.annotation.Nonnull;
public class Person {
@Nonnull
private String firstName;
@Nonnull
private String lastName;
@Nonnull
private String email;
@Nonnull
private Date birthday;
@Nonnull
private Integer id;
@Nonnull
private Boolean subscriber;
@Nonnull
private String membership;
@Nonnull
private String pictureUrl;
@Nonnull
private String profession;
@Nonnull
private Address address;
private Integer managerId;
@Nonnull
private Boolean manager;
@Nonnull
private String status;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFullName() {
return firstName + " " + lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public boolean isSubscriber() {
return subscriber;
}
public void setSubscriber(boolean subscriber) {
this.subscriber = subscriber;
}
public String getMembership() {
return membership;
}
public void setMembership(String membership) {
this.membership = membership;
}
public String getPictureUrl() {
return pictureUrl;
}
public void setPictureUrl(String pictureUrl) {
this.pictureUrl = pictureUrl;
}
public String getProfession() {
return profession;
}
public void setProfession(String profession) {
this.profession = profession;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
return id == other.id;
}
public Integer getManagerId() {
return managerId;
}
public void setManagerId(Integer managerId) {
this.managerId = managerId;
}
public boolean isManager() {
return manager;
}
public void setManager(boolean manager) {
this.manager = manager;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
tree-grid-column.tsx
import React, { useMemo } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { Button } from '@vaadin/react-components/Button.js';
import {
Grid,
type GridDataProviderCallback,
type GridDataProviderParams,
} from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { GridTreeColumn } from '@vaadin/react-components/GridTreeColumn.js';
import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
function Example() {
const managers = useSignal<Person[]>([]);
const expandedItems = useSignal<Person[]>([]);
const dataProvider = useMemo(
() =>
async (
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) => {
const { people, hierarchyLevelSize } = await getPeople({
count: params.pageSize,
startIndex: params.page * params.pageSize,
managerId: params.parentItem ? params.parentItem.id : null,
});
if (!managers.value.length && !params.parentItem) {
managers.value = people;
}
callback(people, hierarchyLevelSize);
},
[]
);
const expandAll = () => {
expandedItems.value = [...managers.value];
};
const collapseAll = () => {
expandedItems.value = [];
};
return (
<>
<HorizontalLayout
style={{ alignItems: 'center', height: 'var(--lumo-size-xl)' }}
theme="spacing"
>
<h3 style={{ flexGrow: 1, margin: 0 }}>Employee</h3>
<Button onClick={expandAll}>Expand All</Button>
<Button onClick={collapseAll}>Collapse All</Button>
</HorizontalLayout>
<Grid
dataProvider={dataProvider}
itemIdPath="id"
itemHasChildrenPath="manager"
expandedItems={expandedItems.value}
>
<GridTreeColumn path="firstName" />
<GridColumn path="lastName" />
<GridColumn path="email" />
</Grid>
</>
);
}
tree-grid-column.ts
import '@vaadin/button';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-tree-column.js';
import '@vaadin/horizontal-layout';
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import type { GridDataProviderCallback, GridDataProviderParams } from '@vaadin/grid';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('tree-grid-column')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
private managers: Person[] = [];
private dataProvider = async (
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) => {
const { people, hierarchyLevelSize } = await getPeople({
count: params.pageSize,
startIndex: params.page * params.pageSize,
managerId: params.parentItem ? params.parentItem.id : null,
});
if (!params.parentItem) {
this.managers = people;
}
callback(people, hierarchyLevelSize);
};
@state()
private expandedItems: unknown[] = [];
protected override render() {
return html`
<vaadin-horizontal-layout
style="align-items: center; height: var(--lumo-size-xl);"
theme="spacing"
>
<h3 style="flex-grow: 1; margin: 0;">Employee</h3>
<vaadin-button @click="${this.expandAll}">Expand All</vaadin-button>
<vaadin-button @click="${this.collapseAll}">Collapse All</vaadin-button>
</vaadin-horizontal-layout>
<vaadin-grid
.dataProvider="${this.dataProvider}"
.itemIdPath="${'id'}"
.itemHasChildrenPath="${'manager'}"
.expandedItems="${this.expandedItems}"
>
<vaadin-grid-tree-column path="firstName"></vaadin-grid-tree-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
</vaadin-grid>
`;
}
private expandAll() {
this.expandedItems = [...this.managers];
}
private collapseAll() {
this.expandedItems = [];
}
}
Rich Content
Like Grid, Tree Grid supports rich content.
new tab
Source code
TreeGridRichContent.java
package com.vaadin.demo.component.treegrid;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.treegrid.TreeGrid;
import com.vaadin.flow.router.Route;
import java.util.List;
@Route("tree-grid-rich-content")
public class TreeGridRichContent extends Div {
private List<Person> managers = DataService.getManagers();
public TreeGridRichContent() {
TreeGrid<Person> treeGrid = new TreeGrid<>();
treeGrid.setItems(managers, this::getStaff);
treeGrid.addComponentHierarchyColumn(person -> {
Avatar avatar = new Avatar();
avatar.setName(person.getFullName());
avatar.setImage(person.getPictureUrl());
Span fullName = new Span(person.getFullName());
Span profession = new Span(person.getProfession());
profession.getStyle()
.set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)");
VerticalLayout column = new VerticalLayout(fullName, profession);
column.getStyle().set("line-height", "var(--lumo-line-height-m)");
column.setPadding(false);
column.setSpacing(false);
HorizontalLayout row = new HorizontalLayout(avatar, column);
row.setAlignItems(FlexComponent.Alignment.CENTER);
row.setSpacing(true);
return row;
}).setHeader("Employee");
treeGrid.addComponentColumn(person -> {
Icon emailIcon = createIcon(VaadinIcon.ENVELOPE);
Span email = new Span(person.getEmail());
Anchor emailLink = new Anchor();
emailLink.add(emailIcon, email);
emailLink.setHref("mailto:" + person.getEmail());
emailLink.getStyle().set("align-items", "center").set("display",
"flex");
Icon phoneIcon = createIcon(VaadinIcon.PHONE);
Span phone = new Span(person.getAddress().getPhone());
Anchor phoneLink = new Anchor();
phoneLink.add(phoneIcon, phone);
phoneLink.setHref("tel:" + person.getAddress().getPhone());
phoneLink.getStyle().set("align-items", "center").set("display",
"flex");
VerticalLayout column = new VerticalLayout(emailLink, phoneLink);
column.getStyle().set("font-size", "var(--lumo-font-size-s)")
.set("line-height", "var(--lumo-line-height-m)");
column.setPadding(false);
column.setSpacing(false);
return column;
}).setHeader("Contact");
add(treeGrid);
}
private Icon createIcon(VaadinIcon vaadinIcon) {
Icon icon = vaadinIcon.create();
icon.getStyle().set("margin-inline-end", "var(--lumo-space-s)");
icon.setSize("var(--lumo-icon-size-s)");
return icon;
}
public List<Person> getStaff(Person manager) {
return DataService.getPeople(manager.getId());
}
}
Person.java
package com.vaadin.demo.domain;
import java.util.Date;
import javax.annotation.Nonnull;
public class Person {
@Nonnull
private String firstName;
@Nonnull
private String lastName;
@Nonnull
private String email;
@Nonnull
private Date birthday;
@Nonnull
private Integer id;
@Nonnull
private Boolean subscriber;
@Nonnull
private String membership;
@Nonnull
private String pictureUrl;
@Nonnull
private String profession;
@Nonnull
private Address address;
private Integer managerId;
@Nonnull
private Boolean manager;
@Nonnull
private String status;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFullName() {
return firstName + " " + lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public boolean isSubscriber() {
return subscriber;
}
public void setSubscriber(boolean subscriber) {
this.subscriber = subscriber;
}
public String getMembership() {
return membership;
}
public void setMembership(String membership) {
this.membership = membership;
}
public String getPictureUrl() {
return pictureUrl;
}
public void setPictureUrl(String pictureUrl) {
this.pictureUrl = pictureUrl;
}
public String getProfession() {
return profession;
}
public void setProfession(String profession) {
this.profession = profession;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
return id == other.id;
}
public Integer getManagerId() {
return managerId;
}
public void setManagerId(Integer managerId) {
this.managerId = managerId;
}
public boolean isManager() {
return manager;
}
public void setManager(boolean manager) {
this.manager = manager;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
tree-grid-rich-content.tsx
import '@vaadin/icons';
import React, { useCallback } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { Avatar } from '@vaadin/react-components/Avatar.js';
import {
Grid,
type GridDataProviderCallback,
type GridDataProviderParams,
} from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { GridTreeToggle } from '@vaadin/react-components/GridTreeToggle.js';
import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js';
import { Icon } from '@vaadin/react-components/Icon.js';
import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
async function dataProvider(
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) {
const { people, hierarchyLevelSize } = await getPeople({
count: params.pageSize,
startIndex: params.page * params.pageSize,
managerId: params.parentItem ? params.parentItem.id : null,
});
callback(people, hierarchyLevelSize);
}
function contactRenderer({ item: person }: { item: Person }) {
return (
<VerticalLayout
style={{
fontSize: 'var(--lumo-font-size-s)',
lineHeight: 'var(--lumo-line-height-m)',
}}
>
<a href={`mailto:${person.email}`} style={{ display: 'flex', alignItems: 'center' }}>
<Icon
icon="vaadin:envelope"
style={{
height: 'var(--lumo-icon-size-s)',
marginInlineEnd: 'var(--lumo-space-s)',
width: 'var(--lumo-icon-size-s)',
}}
/>
<span>{person.email}</span>
</a>
<a href={`tel:${person.address.phone}`} style={{ display: 'flex', alignItems: 'center' }}>
<Icon
icon="vaadin:phone"
style={{
height: 'var(--lumo-icon-size-s)',
marginInlineEnd: 'var(--lumo-space-s)',
width: 'var(--lumo-icon-size-s)',
}}
/>
<span>{person.address.phone}</span>
</a>
</VerticalLayout>
);
}
function Example() {
const expandedItems = useSignal<Person[]>([]);
const toggleRenderer = useCallback(
({ item: person, model }: { item: Person; model: { level?: number; expanded?: boolean } }) => (
<GridTreeToggle
leaf={!person.manager}
level={model?.level ?? 0}
expanded={!!model?.expanded}
onClick={(e) => {
if (!e.defaultPrevented) {
return;
}
if (e.currentTarget.expanded) {
expandedItems.value = [...expandedItems.value, person];
} else {
expandedItems.value = expandedItems.value.filter((p) => p.id !== person.id);
}
}}
>
<HorizontalLayout style={{ alignItems: 'center' }} theme="spacing">
<Avatar img={person.pictureUrl} name={`${person.firstName} ${person.lastName}`} />
<VerticalLayout style={{ lineHeight: 'var(--lumo-line-height-m)' }}>
<span>
{person.firstName} {person.lastName}
</span>
<span
style={{
fontSize: 'var(--lumo-font-size-s)',
color: 'var(--lumo-secondary-text-color)',
}}
>
{person.profession}
</span>
</VerticalLayout>
</HorizontalLayout>
</GridTreeToggle>
),
[]
);
return (
<Grid dataProvider={dataProvider} expandedItems={expandedItems.value}>
<GridColumn autoWidth header="Employee" renderer={toggleRenderer} />
<GridColumn autoWidth header="Contact" renderer={contactRenderer} />
</Grid>
);
}
tree-grid-rich-content.ts
import '@vaadin/avatar';
import '@vaadin/button';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-tree-toggle.js';
import '@vaadin/horizontal-layout';
import '@vaadin/icon';
import '@vaadin/icons';
import '@vaadin/vertical-layout';
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import type { GridDataProviderCallback, GridDataProviderParams } from '@vaadin/grid';
import type { GridColumnBodyLitRenderer } from '@vaadin/grid/lit.js';
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
import type { GridTreeToggleExpandedChangedEvent } from '@vaadin/grid/vaadin-grid-tree-toggle.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('tree-grid-rich-content')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private expandedItems: Person[] = [];
async dataProvider(
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) {
const { people, hierarchyLevelSize } = await getPeople({
count: params.pageSize,
startIndex: params.page * params.pageSize,
managerId: params.parentItem ? params.parentItem.id : null,
});
callback(people, hierarchyLevelSize);
}
private employeeRenderer: GridColumnBodyLitRenderer<Person> = (person, model) => html`
<vaadin-grid-tree-toggle
.leaf="${!person.manager}"
.level="${model.level ?? 0}"
@expanded-changed="${(e: GridTreeToggleExpandedChangedEvent) => {
if (e.detail.value) {
this.expandedItems = [...this.expandedItems, person];
} else {
this.expandedItems = this.expandedItems.filter((p) => p.id !== person.id);
}
}}"
.expanded="${!!model.expanded}"
>
<vaadin-horizontal-layout style="align-items: center;" theme="spacing">
<vaadin-avatar
img="${person.pictureUrl}"
name="${`${person.firstName} ${person.lastName}`}"
></vaadin-avatar>
<vaadin-vertical-layout style="line-height: var(--lumo-line-height-m);">
<span>${person.firstName} ${person.lastName}</span>
<span
style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);"
>
${person.profession}
</span>
</vaadin-vertical-layout>
</vaadin-horizontal-layout>
</vaadin-grid-tree-toggle>
`;
private contactRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
<vaadin-vertical-layout
style="font-size: var(--lumo-font-size-s); line-height: var(--lumo-line-height-m);"
>
<a href="mailto:${person.email}" style="align-items: center; display: flex;">
<vaadin-icon
icon="vaadin:envelope"
style="height: var(--lumo-icon-size-s); margin-inline-end: var(--lumo-space-s); width: var(--lumo-icon-size-s);"
></vaadin-icon>
<span>${person.email}</span>
</a>
<a href="tel:${person.address.phone}" style="align-items: center; display: flex;">
<vaadin-icon
icon="vaadin:phone"
style="height: var(--lumo-icon-size-s); margin-inline-end: var(--lumo-space-s); width: var(--lumo-icon-size-s);"
></vaadin-icon>
<span>${person.address.phone}</span>
</a>
</vaadin-vertical-layout>
`;
protected override render() {
return html`
<vaadin-grid .dataProvider="${this.dataProvider}" .expandedItems="${this.expandedItems}">
<vaadin-grid-column
auto-width
header="Employee"
${columnBodyRenderer(this.employeeRenderer, [])}
></vaadin-grid-column>
<vaadin-grid-column
auto-width
header="Contact"
${columnBodyRenderer(this.contactRenderer, [])}
></vaadin-grid-column>
</vaadin-grid>
`;
}
}
Programmatic Scrolling
Grid supports programmatic navigation to a specific row. This is particularly useful when dealing with large data sets. It saves users from having to scroll through potentially hundreds or thousands of rows.
To use this feature, you need to specify the index of the row you want to view. The scroll position of the grid will then be adjusted to bring that row into view.
With multiple levels of hierarchy, you need to specify the row index for each level, separately. For example, to scroll to the second child-row (index 1) of the third root-level row (index 2), you would provide the indexes 2, 1.
new tab
Source code
TreeGridScrollToIndex.java
package com.vaadin.demo.component.treegrid;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.treegrid.TreeGrid;
import com.vaadin.flow.data.provider.hierarchy.AbstractHierarchicalDataProvider;
import com.vaadin.flow.data.provider.hierarchy.HierarchicalQuery;
import com.vaadin.flow.router.Route;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
@Route("tree-grid-scroll-to-index")
public class TreeGridScrollToIndex extends Div {
private IntegerField parentIndexField = new IntegerField("Parent index");
private IntegerField childIndexField = new IntegerField("Child index");
private Button scrollToIndexButton = new Button();
private Map<Person, List<Integer>> personToIndexAddress = new HashMap<>();
private TreeGrid<Person> treeGrid = new TreeGrid<>();
public TreeGridScrollToIndex() {
treeGrid.setDataProvider(new LazyLoadingProvider());
treeGrid.setUniqueKeyDataGenerator("key", (person) -> {
return String.valueOf(person.getId());
});
treeGrid.expand(DataService.getManagers());
treeGrid.addHierarchyColumn(Person::getFirstName).setWidth("200px")
.setFlexGrow(0).setHeader("First name");
treeGrid.addSelectionListener(e -> {
if (e.getFirstSelectedItem().isPresent()) {
Person person = e.getFirstSelectedItem().get();
List<Integer> indexAddress = personToIndexAddress.get(person);
if (indexAddress != null) {
parentIndexField.setValue(indexAddress.get(0));
if (indexAddress.size() > 1) {
childIndexField.setValue(indexAddress.get(1));
}
}
}
});
treeGrid.addColumn(person -> StringUtils
.join(personToIndexAddress.get(person), ", ")).setWidth("80px")
.setFlexGrow(0).setHeader("Index");
treeGrid.addColumn(Person::getEmail).setHeader("Email");
add(treeGrid);
HorizontalLayout controls = new HorizontalLayout();
controls.setSpacing(true);
controls.setAlignItems(Alignment.END);
parentIndexField.setWidth("120px");
childIndexField.setWidth("120px");
parentIndexField.setMin(0);
childIndexField.setMin(0);
parentIndexField.setStepButtonsVisible(true);
childIndexField.setStepButtonsVisible(true);
parentIndexField.setValue(13);
childIndexField.setValue(6);
parentIndexField.addValueChangeListener(e -> updateSelectedItem());
childIndexField.addValueChangeListener(e -> updateSelectedItem());
controls.add(parentIndexField);
controls.add(childIndexField);
scrollToIndexButton.addClickListener(e -> {
int[] indexesToScrollTo = { parentIndexField.getValue(),
childIndexField.getValue() };
treeGrid.scrollToIndex(indexesToScrollTo);
});
controls.add(scrollToIndexButton);
add(controls);
}
private void updateSelectedItem() {
treeGrid.select(null);
Integer parentIndex = parentIndexField.getValue();
Integer childIndex = childIndexField.getValue();
personToIndexAddress.entrySet().stream().filter(entry -> {
List<Integer> indexes = entry.getValue();
return indexes.size() == 2
&& List.of(parentIndex, childIndex).equals(indexes);
}).findFirst().ifPresent(entry -> {
treeGrid.select(entry.getKey());
});
scrollToIndexButton
.setText("Scroll to index: " + parentIndex + ", " + childIndex);
}
private class LazyLoadingProvider
extends AbstractHierarchicalDataProvider<Person, Void> {
@Override
public int getChildCount(HierarchicalQuery<Person, Void> query) {
return (int) this.fetchChildren(query).count();
}
@Override
public Stream<Person> fetchChildren(
HierarchicalQuery<Person, Void> query) {
List<Person> people;
if (query.getParent() == null) {
people = DataService.getManagers();
} else {
people = DataService.getPeople(query.getParent().getId());
}
int limit = query.getLimit();
int offset = query.getOffset();
// Cache the index address of each person for demo purposes
AtomicInteger personIndex = new AtomicInteger(0);
people.stream().skip(offset).limit(limit).forEach(person -> {
int index = offset + personIndex.getAndIncrement();
List<Integer> parentIndexAddress = personToIndexAddress
.get(query.getParent());
List<Integer> indexAddress = parentIndexAddress == null
? List.of(index)
: List.of(parentIndexAddress.get(0), index);
personToIndexAddress.put(person, indexAddress);
});
updateSelectedItem();
return people.stream().skip(offset).limit(limit);
}
@Override
public boolean hasChildren(Person item) {
return DataService.getPeople(item.getId()).size() > 0;
}
@Override
public boolean isInMemory() {
return false;
}
}
}
tree-grid-scroll-to-index.tsx
import React, { useMemo, useRef } from 'react';
import { useComputed, useSignal } from '@vaadin/hilla-react-signals';
import { Button } from '@vaadin/react-components/Button.js';
import {
Grid,
type GridDataProviderCallback,
type GridDataProviderParams,
type GridElement,
} from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { GridTreeColumn } from '@vaadin/react-components/GridTreeColumn.js';
import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js';
import { IntegerField } from '@vaadin/react-components/IntegerField.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
type PersonOrId =
| Person
| {
id: number;
};
function Example() {
const gridRef = useRef<GridElement>(null);
const idToIndexes = useMemo(() => new Map<number, number[]>(), []);
const expandedItems = useSignal<Person[]>([]);
const indexesToScrollTo = useSignal<number[]>([13, 6]);
const indexesToScrollToRef = useRef<number[]>(indexesToScrollTo.value);
indexesToScrollToRef.current = indexesToScrollTo.value;
const dataProvider = useMemo(
() =>
async (
params: GridDataProviderParams<PersonOrId>,
callback: GridDataProviderCallback<PersonOrId>
) => {
const startIndex = params.page * params.pageSize;
const { people, hierarchyLevelSize } = await getPeople({
count: params.pageSize,
startIndex,
managerId: params.parentItem ? params.parentItem.id : null,
});
// Cache the index address of each person for demo purposes
people.forEach((person, idx) => {
const index = startIndex + idx;
const parentIndexes = params.parentItem
? (idToIndexes.get(params.parentItem.id) ?? [])
: [];
const indexAddress = [...parentIndexes, index];
idToIndexes.set(person.id, indexAddress);
if (
indexAddress[0] === indexesToScrollToRef.current[0] &&
indexAddress[1] === indexesToScrollToRef.current[1]
) {
indexesToScrollTo.value = indexAddress;
}
});
if (!expandedItems.value.length && !params.parentItem) {
// Expand the root level by default
expandedItems.value = people;
}
callback(people, hierarchyLevelSize);
},
[]
);
const selectedItems = useComputed(() => {
const indexAddress = indexesToScrollTo.value.join(', ');
const id = Array.from(idToIndexes.entries()).find(
([, indexes]) => indexes.join(', ') === indexAddress
)?.[0];
return id ? [{ id }] : [];
});
return (
<>
<Grid
ref={gridRef}
itemIdPath="id"
itemHasChildrenPath="manager"
dataProvider={dataProvider}
expandedItems={expandedItems.value}
selectedItems={selectedItems.value}
onActiveItemChanged={(e) => {
if (e.detail.value) {
indexesToScrollTo.value = idToIndexes.get(e.detail.value.id) ?? [];
}
}}
>
<GridTreeColumn<Person> path="firstName" width="200px" flexGrow={0} />
<GridColumn<Person> header="Index" width="80px" flexGrow={0}>
{({ item }) => idToIndexes.get(item.id)?.join(', ')}
</GridColumn>
<GridColumn<Person> path="email" />
</Grid>
<HorizontalLayout theme="spacing" className="items-end">
<IntegerField
label="Parent index"
stepButtonsVisible
min={0}
style={{ width: '120px' }}
value={String(indexesToScrollTo.value[0])}
onChange={(e) => {
indexesToScrollTo.value = [parseInt(e.target.value) || 0, indexesToScrollTo.value[1]];
}}
/>
<IntegerField
label="Child index"
stepButtonsVisible
min={0}
style={{ width: '120px' }}
value={String(indexesToScrollTo.value[1])}
onChange={(e) => {
indexesToScrollTo.value = [indexesToScrollTo.value[0], parseInt(e.target.value) || 0];
}}
/>
<Button
onClick={() => {
const grid = gridRef.current;
if (grid) {
grid.scrollToIndex(...indexesToScrollTo.value);
}
}}
>
Scroll to index: {indexesToScrollTo.value.join(', ')}
</Button>
</HorizontalLayout>
</>
);
}
tree-grid-scroll-to-index.ts
import '@vaadin/button';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-tree-column.js';
import '@vaadin/horizontal-layout';
import '@vaadin/integer-field';
import { html, LitElement } from 'lit';
import { customElement, query, state } from 'lit/decorators.js';
import type {
Grid,
GridActiveItemChangedEvent,
GridBodyRenderer,
GridDataProviderCallback,
GridDataProviderParams,
} from '@vaadin/grid';
import type { IntegerFieldChangeEvent } from '@vaadin/integer-field';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('tree-grid-scroll-to-index')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@query('vaadin-grid')
private grid!: Grid<Person>;
@state()
private expandedItems?: Person[];
@state()
private indexesToScrollTo: number[] = [13, 6];
@state()
private idToIndexes = new Map<number, number[]>();
constructor() {
super();
this.dataProvider = this.dataProvider.bind(this);
}
async dataProvider(
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) {
const startIndex = params.page * params.pageSize;
const { people, hierarchyLevelSize } = await getPeople({
count: params.pageSize,
startIndex,
managerId: params.parentItem ? params.parentItem.id : null,
});
// Cache the index address of each person for demo purposes
people.forEach((person, idx) => {
const index = startIndex + idx;
const parentIndexes = params.parentItem
? (this.idToIndexes.get(params.parentItem.id) ?? [])
: [];
const indexes = [...parentIndexes, index];
this.idToIndexes = new Map(this.idToIndexes).set(person.id, indexes);
});
if (!this.expandedItems && !params.parentItem) {
// Expand the root level by default
this.expandedItems = people;
}
callback(people, hierarchyLevelSize);
}
private indexRenderer: GridBodyRenderer<Person> = (root, _, { item }) => {
root.textContent = this.idToIndexes.get(item.id)?.join(', ') ?? '';
};
private getSelectedItems(indexes: number[], idToIndexes: Map<number, number[]>) {
const id = Array.from(idToIndexes.entries()).find(
([, idxs]) => idxs[0] === indexes[0] && idxs[1] === indexes[1]
)?.[0];
return id ? [{ id }] : [];
}
protected override render() {
return html`
<vaadin-grid
item-id-path="id"
item-has-children-path="manager"
.dataProvider="${this.dataProvider}"
.expandedItems="${this.expandedItems ?? []}"
.selectedItems="${this.getSelectedItems(this.indexesToScrollTo, this.idToIndexes)}"
@active-item-changed=${(e: GridActiveItemChangedEvent<Person>) => {
if (e.detail.value) {
this.indexesToScrollTo = this.idToIndexes.get(e.detail.value.id) ?? [];
}
}}
>
<vaadin-grid-tree-column
path="firstName"
width="200px"
flex-grow="0"
></vaadin-grid-tree-column>
<vaadin-grid-column
header="Index"
.renderer=${this.indexRenderer}
width="80px"
flex-grow="0"
></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
</vaadin-grid>
<vaadin-horizontal-layout theme="spacing" class="items-end">
<vaadin-integer-field
label="Parent index"
step-buttons-visible
min="0"
style="width: 120px"
.value=${String(this.indexesToScrollTo[0] ?? '')}
@change=${(e: IntegerFieldChangeEvent) => {
this.indexesToScrollTo = [parseInt(e.target.value) || 0, this.indexesToScrollTo[1]];
}}
></vaadin-integer-field>
<vaadin-integer-field
label="Child index"
step-buttons-visible
min="0"
style="width: 120px"
.value=${String(this.indexesToScrollTo[1] ?? '')}
@change=${(e: IntegerFieldChangeEvent) => {
this.indexesToScrollTo = [this.indexesToScrollTo[0], parseInt(e.target.value) || 0];
}}
></vaadin-integer-field>
<vaadin-button
@click=${() => {
this.grid.scrollToIndex(...this.indexesToScrollTo);
}}
>
Scroll to index: ${this.indexesToScrollTo.join(', ')}
</vaadin-button>
</vaadin-horizontal-layout>
`;
}
}