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() };
// tag::snippet[]
treeGrid.scrollToIndex(indexesToScrollTo);
// end::snippet[]
});
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) {
// tag::snippet[]
grid.scrollToIndex(...indexesToScrollTo.value);
// end::snippet[]
}
}}
>
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=${() => {
// tag::snippet[]
this.grid.scrollToIndex(...this.indexesToScrollTo);
// end::snippet[]
}}
>
Scroll to index: ${this.indexesToScrollTo.join(', ')}
</vaadin-button>
</vaadin-horizontal-layout>
`;
}
}