Skip to content
8 changes: 8 additions & 0 deletions articles/components/tree-grid/columns.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Columns
page-title: Configuring columns of the Vaadin Tree Grid component
meta-description: Learn about aligning, freezing, grouping, headers and footers, visibility, and width of the Vaadin Tree Grid component's columns.
order: 10
---

See <<../grid/columns#,Grid>> for details on columns.
32 changes: 31 additions & 1 deletion articles/components/tree-grid/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,10 @@ include::{root}/src/main/java/com/vaadin/demo/component/treegrid/TreeGridScrollT
- [methodname]`getItemIndex(T item, HierarchicalQuery query)`

=== Scrolling to an Item by Path
You can also scroll to an item by providing its hierarchical path. With multiple levels of hierarchy, the path depends on the hierarchy format of the data provider.

You can also scroll to an item by providing its hierarchical path – an array of indexes where each index refers to a child of the item at the previous index. For example, to scroll to the second child-row (index 1) of the third root-level row (index 2), you would provide the path 2, 1.
- If the data provider uses nested hierarchy format, you need to specify an array of indexes where each index refers to a child of the item at the previous index. For example, to scroll to the second child-row (index 1) of the third root-level row (index 2), you would provide the path 2, 1.
- If the data provider uses flat hierarchy format, you need to specify the flat index of the item. The flat index only takes reachable items into account. Therefore, in order to reach the children of collapsed items, you need to expand them first. For example, to scroll to the second child-row (index 1) of the third root-level row (index 2) assuming only the ancestors of the item are expanded, you would provide the flat index 4.

Scrolling continues until it reaches the last index in the array or encounters a collapsed item.

Expand Down Expand Up @@ -179,6 +181,34 @@ include::{root}/frontend/demo/component/tree-grid/react/tree-grid-scroll-to-inde
endif::[]
--

== Drag & Drop

Tree Grid supports drag-and-drop operations. You can enable and handle them in your logic, for example, to allow users to move rows from one parent node to another:

[.example]
--
ifdef::lit[]
[source,typescript]
----
include::{root}/frontend/demo/component/tree-grid/tree-grid-drag-drop.ts[render,tags=snippet,indent=0,group=Lit]
----
endif::[]

ifdef::flow[]
[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/component/treegrid/TreeGridDragDrop.java[render,tags=snippet,indent=0,group=Flow]
----
endif::[]

ifdef::react[]
[source,tsx]
----
include::{root}/frontend/demo/component/tree-grid/react/tree-grid-drag-drop.tsx[render,tags=snippet,indent=0,group=React]
----
endif::[]
--


== Related Components

Expand Down
8 changes: 8 additions & 0 deletions articles/components/tree-grid/renderers.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Renderers
page-title: Using renderers in the Vaadin Tree Grid component
meta-description: Use renderers to render the contents of a Vaadin Tree Grid component's specific columns using components and native HTML elements.
order: 20
---

See <<../grid/renderers#,Grid>> for details on renderers.
6 changes: 6 additions & 0 deletions articles/components/tree-grid/selection.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Selection
order: 40
---

See <<../grid/selection#,Grid>> for details on selection.
91 changes: 91 additions & 0 deletions frontend/demo/component/tree-grid/react/tree-grid-drag-drop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line
import React, { useEffect, useMemo } from 'react';
import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line
import { useSignal } from '@vaadin/hilla-react-signals';
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';

function Example() {
useSignals(); // hidden-source-line
// tag::snippet[]
const draggedItem = useSignal<Person | undefined>(undefined);
const items = useSignal<Person[]>([]);
const expandedItems = useSignal<Person[]>([]);

useEffect(() => {
getPeople().then(({ people }) => {
items.value = people;
});
}, []);

const dataProvider = useMemo(
() => (params: GridDataProviderParams<Person>, callback: GridDataProviderCallback<Person>) => {
const { page, pageSize, parentItem } = params;
const startIndex = page * pageSize;
const endIndex = startIndex + pageSize;

/*
We cannot change the underlying data in this demo so this dataProvider uses
a local field to fetch its values. This allows us to keep a reference to the
modified list instead of loading a new list every time the dataProvider gets
called. In a real application, you should always access your data source
here and avoid using grid.clearCache() whenever possible.
*/
const result = parentItem
? items.value.filter((item) => item.managerId === parentItem.id)
: items.value.filter((item) => item.manager).slice(startIndex, endIndex);

callback(result, result.length);
},
[items.value]
);

return (
<Grid
dataProvider={dataProvider}
itemIdPath="id"
itemHasChildrenPath="manager"
expandedItems={expandedItems.value}
onExpandedItemsChanged={(event) => {
expandedItems.value = event.detail.value;
}}
rowsDraggable
dropMode={draggedItem.value ? 'on-top' : undefined}
onGridDragstart={(event) => {
draggedItem.value = event.detail.draggedItems[0];
}}
onGridDragend={() => {
draggedItem.value = undefined;
}}
onGridDrop={(event) => {
const manager = event.detail.dropTargetItem;
if (draggedItem.value) {
draggedItem.value.managerId = manager.id;
items.value = [...items.value];
}
}}
dragFilter={(model) => {
const item = model.item;
return !item.manager;
}}
dropFilter={(model) => {
const item = model.item;
return item.manager && item.id !== draggedItem.value?.managerId;
}}
>
<GridTreeColumn path="firstName" />
<GridColumn path="lastName" />
<GridColumn path="email" />
</Grid>
);
// end::snippet[]
}

export default reactExample(Example); // hidden-source-line
121 changes: 121 additions & 0 deletions frontend/demo/component/tree-grid/tree-grid-drag-drop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'Frontend/demo/init'; // hidden-source-line
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-tree-column.js';
import { html, LitElement } from 'lit';
import { customElement, query, state } from 'lit/decorators.js';
import type {
Grid,
GridDataProviderCallback,
GridDataProviderParams,
GridDragStartEvent,
GridDropEvent,
GridExpandedItemsChangedEvent,
GridItemModel,
} from '@vaadin/grid';
import { getPeople } from 'Frontend/demo/domain/DataService';
import { applyTheme } from 'Frontend/demo/theme';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';

// tag::snippet[]
@customElement('tree-grid-drag-drop')
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 draggedItem: Person | undefined;

@state()
private items: Person[] = [];

@state()
private managers: Person[] = [];

@state()
private expandedItems: Person[] = [];

protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people;
this.managers = this.items.filter((item) => item.manager);
// Avoid using this method
this.grid.clearCache();
}

private dataProvider = (
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) => {
const { page, pageSize, parentItem } = params;
const startIndex = page * pageSize;
const endIndex = startIndex + pageSize;

/*
We cannot change the underlying data in this demo so this dataProvider uses
a local field to fetch its values. This allows us to keep a reference to the
modified list instead of loading a new list every time the dataProvider gets
called. In a real application, you should always access your data source
here and avoid using grid.clearCache() whenever possible.
*/
const result = parentItem
? this.items.filter((item) => item.managerId === parentItem.id)
: this.managers.slice(startIndex, endIndex);

callback(result, result.length);
};

protected override render() {
return html`
<vaadin-grid
.dataProvider="${this.dataProvider}"
.itemIdPath="${'id'}"
.itemHasChildrenPath="${'manager'}"
.expandedItems="${this.expandedItems}"
@expanded-items-changed="${(event: GridExpandedItemsChangedEvent<Person>) => {
this.expandedItems = event.detail.value;
}}"
rows-draggable
.dropMode=${this.draggedItem ? 'on-top' : undefined}
@grid-dragstart="${(event: GridDragStartEvent<Person>) => {
this.draggedItem = event.detail.draggedItems[0];
}}"
@grid-dragend="${() => {
this.draggedItem = undefined;
}}"
@grid-drop="${(event: GridDropEvent<Person>) => {
const manager = event.detail.dropTargetItem;
if (this.draggedItem) {
// In a real application, when using a data provider, you should
// change the persisted data instead of updating a field
this.draggedItem.managerId = manager.id;
// Avoid using this method
this.grid.clearCache();
}
}}"
.dragFilter="${(model: GridItemModel<Person>) => {
const item = model.item;
return !item.manager; // Only drag non-managers
}}"
.dropFilter="${(model: GridItemModel<Person>) => {
const item = model.item;
return (
item.manager && // Can only drop on a supervisor
item.id !== this.draggedItem?.managerId // Disallow dropping on the same manager
);
}}"
>
<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>
`;
}
}
// end::snippet[]
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.popover.Popover;
import com.vaadin.flow.component.popover.PopoverPosition;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.router.Route;
Expand Down
105 changes: 105 additions & 0 deletions src/main/java/com/vaadin/demo/component/treegrid/TreeGridDragDrop.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.vaadin.demo.component.treegrid;

import com.vaadin.demo.DemoExporter; // hidden-source-line
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.grid.dnd.GridDropMode;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.treegrid.TreeGrid;
import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider;
import com.vaadin.flow.data.provider.hierarchy.TreeData;
import com.vaadin.flow.data.provider.hierarchy.TreeDataProvider;
import com.vaadin.flow.router.Route;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Route("tree-grid-drag-drop")
public class TreeGridDragDrop extends Div {

private final List<Person> managers;
private final Map<Integer, List<Person>> staffGroupedByMangers;

private Person draggedItem;

public TreeGridDragDrop() {
List<Person> people = DataService.getPeople();
managers = people.stream().filter(Person::isManager)
.toList();
staffGroupedByMangers = people.stream()
.filter(person -> person.getManagerId() != null)
.collect(Collectors.groupingBy(Person::getManagerId,
Collectors.toList()));

// tag::snippet[]
TreeGrid<Person> treeGrid = setupTreeGrid();

TreeData<Person> treeData = new TreeData<>();
treeData.addItems(managers, this::getStaff);

// For drag-and-drop use cases, it is recommended to use data providers that
// return hierarchical data in HierarchyFormat.FLATTENED. This format allows
// TreeGrid to maintain the scroll position after refreshAll(), avoiding the
// scroll jumps that can otherwise occur with HierarchyFormat.NESTED (default)
// which requires each hierarchy level to be requested separately, on demand.
TreeDataProvider<Person> treeDataProvider = new TreeDataProvider<>(
treeData , HierarchicalDataProvider.HierarchyFormat.FLATTENED);
treeGrid.setDataProvider(treeDataProvider);

// Enable drag-and-drop
treeGrid.setRowsDraggable(true);
// Only allow dragging staff
treeGrid.setDragFilter(person -> !person.isManager());
// Only allow dropping on managers
treeGrid.setDropFilter(Person::isManager);

treeGrid.addDragStartListener(e -> {
treeGrid.setDropMode(GridDropMode.ON_TOP);
draggedItem = e.getDraggedItems().get(0);
});

treeGrid.addDropListener(e -> {
Person newManager = e.getDropTargetItem().orElse(null);
boolean isSameManager = newManager != null
&& newManager.getId().equals(draggedItem.getManagerId());

if (newManager == null || isSameManager)
return;

draggedItem.setManagerId(newManager.getId());
treeData.setParent(draggedItem, newManager);

// Reset TreeGrid's cache to trigger a re-render
treeDataProvider.refreshAll();
});

treeGrid.addDragEndListener(e -> {
treeGrid.setDropMode(null);
draggedItem = null;
});
// end::snippet[]

add(treeGrid);
}

private static TreeGrid<Person> setupTreeGrid() {
TreeGrid<Person> treeGrid = new TreeGrid<>();
treeGrid.addHierarchyColumn(Person::getFirstName)
.setHeader("First name");
treeGrid.addColumn(Person::getLastName).setHeader("Last name");
treeGrid.addColumn(Person::getEmail).setHeader("Email");

return treeGrid;
}

private List<Person> getStaff(Person manager) {
return staffGroupedByMangers.getOrDefault(manager.getId(),
Collections.emptyList());
}

public static class Exporter // hidden-source-line
extends DemoExporter<TreeGridDragDrop> { // hidden-source-line
} // hidden-source-line
}
Loading