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.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);
// tag::snippet[]
treeGrid.addComponentHierarchyColumn(person -> {
Avatar avatar = new Avatar(person.getFullName(),
person.getPictureUrl());
avatar.getStyle().set("--vaadin-avatar-size", "2.25rem");
Span name = new Span(person.getFullName());
Span profession = new Span(person.getProfession());
Div personItem = new Div(avatar, name, profession);
personItem.addClassName("person-item");
return personItem;
}).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", "0.875rem").set("line-height",
"1.625");
column.setPadding(false);
column.setSpacing(false);
return column;
}).setHeader("Contact");
// end::snippet[]
add(treeGrid);
}
private Icon createIcon(VaadinIcon vaadinIcon) {
Icon icon = vaadinIcon.create();
icon.getStyle().set("margin-inline-end", "var(--vaadin-gap-s)");
icon.setSize("1.25em");
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 jakarta.annotation.Nonnull;
// tag::snippet[]
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;
}
}
// end::snippet[]
person-item.css
.person-item {
display: grid;
grid-template-columns: min-content auto;
grid-template-rows: auto auto;
grid-template-areas:
"avatar name"
"avatar title";
gap: 0 var(--vaadin-gap-s);
align-items: center;
line-height: 1;
& > :is(vaadin-avatar, img) {
grid-area: avatar;
}
& > span:first-of-type {
grid-area: name;
}
& > span:last-of-type {
grid-area: title;
font-size: 0.875rem;
color: var(--vaadin-text-color-secondary);
}
}
person-item.css
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 { 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);
}
// tag::snippet[]
function contactRenderer({ item: person }: { item: Person }) {
return (
<VerticalLayout
style={{
fontSize: '.875rem',
lineHeight: '1.625',
}}
>
<a href={`mailto:${person.email}`} style={{ display: 'flex', alignItems: 'center' }}>
<Icon
icon="vaadin:envelope"
style={{
width: '1.25em',
height: '1.25em',
marginInlineEnd: 'var(--vaadin-gap-s)',
}}
/>
<span>{person.email}</span>
</a>
<a href={`tel:${person.address.phone}`} style={{ display: 'flex', alignItems: 'center' }}>
<Icon
icon="vaadin:phone"
style={{
width: '1.25em',
height: '1.25em',
marginInlineEnd: 'var(--vaadin-gap-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);
}
}}
>
<div className="person-item">
<Avatar
img={person.pictureUrl}
name={`${person.firstName} ${person.lastName}`}
style={{ '--vaadin-avatar-size': '2.25rem' }}
/>
<span>
{person.firstName} {person.lastName}
</span>
<span>{person.profession}</span>
</div>
</GridTreeToggle>
),
[]
);
return (
<Grid dataProvider={dataProvider} expandedItems={expandedItems.value}>
<GridColumn autoWidth header="Employee" renderer={toggleRenderer} />
<GridColumn autoWidth header="Contact" renderer={contactRenderer} />
</Grid>
);
}
// end::snippet[]
person-item.css
.person-item {
display: grid;
grid-template-columns: min-content auto;
grid-template-rows: auto auto;
grid-template-areas:
"avatar name"
"avatar title";
gap: 0 var(--vaadin-gap-s);
align-items: center;
line-height: 1;
& > :is(vaadin-avatar, img) {
grid-area: avatar;
}
& > span:first-of-type {
grid-area: name;
}
& > span:last-of-type {
grid-area: title;
font-size: 0.875rem;
color: var(--vaadin-text-color-secondary);
}
}
person-item.css
tree-grid-rich-content.ts
import '@vaadin/avatar';
import '@vaadin/button';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-tree-toggle.js';
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 { applyTheme } from 'Frontend/demo/theme';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
@customElement('tree-grid-rich-content')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
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);
}
// tag::snippet[]
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}"
>
<div class="person-item">
<vaadin-avatar
.img="${person.pictureUrl}"
.name="${`${person.firstName} ${person.lastName}`}"
style="--vaadin-avatar-size: 2.25rem"
></vaadin-avatar>
<span>${person.firstName} ${person.lastName}</span>
<span>${person.profession}</span>
</div>
</vaadin-grid-tree-toggle>
`;
private contactRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
<vaadin-vertical-layout style="font-size: .875rem; line-height: 1.625;">
<a href="mailto:${person.email}" style="align-items: center; display: flex;">
<vaadin-icon
icon="vaadin:envelope"
style="width: 1.25em; height: 1.25em; margin-inline-end: var(--vaadin-gap-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="width: 1.25em; height: 1.25em; margin-inline-end: var(--vaadin-gap-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>
`;
}
// end::snippet[]
}
person-item.css
.person-item {
display: grid;
grid-template-columns: min-content auto;
grid-template-rows: auto auto;
grid-template-areas:
"avatar name"
"avatar title";
gap: 0 var(--vaadin-gap-s);
align-items: center;
line-height: 1;
& > :is(vaadin-avatar, img) {
grid-area: avatar;
}
& > span:first-of-type {
grid-area: name;
}
& > span:last-of-type {
grid-area: title;
font-size: 0.875rem;
color: var(--vaadin-text-color-secondary);
}
}
person-item.css