Skip to content

Commit ef3a0ea

Browse files
vursenugur-vaadin
authored andcommitted
docs: update TreeGrid article and examples
1 parent bb38c9c commit ef3a0ea

File tree

5 files changed

+346
-1
lines changed

5 files changed

+346
-1
lines changed

articles/components/tree-grid/index.adoc

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ endif::[]
123123

124124
== Programmatic Scrolling
125125

126-
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.
126+
Tree 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.
127127

128128
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.
129129

@@ -153,6 +153,34 @@ include::{root}/frontend/demo/component/tree-grid/react/tree-grid-scroll-to-inde
153153
endif::[]
154154
--
155155

156+
== Drag & Drop
157+
158+
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:
159+
160+
[.example]
161+
--
162+
ifdef::lit[]
163+
[source,typescript]
164+
----
165+
include::{root}/frontend/demo/component/tree-grid/tree-grid-drag-drop.ts[render,tags=snippet,indent=0,group=Lit]
166+
----
167+
endif::[]
168+
169+
ifdef::flow[]
170+
[source,java]
171+
----
172+
include::{root}/src/main/java/com/vaadin/demo/component/treegrid/TreeGridDragDrop.java[render,tags=snippet,indent=0,group=Flow]
173+
----
174+
endif::[]
175+
176+
ifdef::react[]
177+
[source,tsx]
178+
----
179+
include::{root}/frontend/demo/component/tree-grid/react/tree-grid-drag-drop.tsx[render,tags=snippet,indent=0,group=React]
180+
----
181+
endif::[]
182+
--
183+
156184

157185
== Related Components
158186

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line
2+
import React, { useEffect, useMemo } from 'react';
3+
import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line
4+
import { useSignal } from '@vaadin/hilla-react-signals';
5+
import {
6+
Grid,
7+
type GridDataProviderCallback,
8+
type GridDataProviderParams,
9+
} from '@vaadin/react-components/Grid.js';
10+
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
11+
import { GridTreeColumn } from '@vaadin/react-components/GridTreeColumn.js';
12+
import { getPeople } from 'Frontend/demo/domain/DataService';
13+
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
14+
15+
function Example() {
16+
useSignals(); // hidden-source-line
17+
// tag::snippet[]
18+
const draggedItem = useSignal<Person | undefined>(undefined);
19+
const items = useSignal<Person[]>([]);
20+
const expandedItems = useSignal<Person[]>([]);
21+
22+
useEffect(() => {
23+
getPeople().then(({ people }) => {
24+
items.value = people;
25+
});
26+
}, []);
27+
28+
const dataProvider = useMemo(
29+
() => (params: GridDataProviderParams<Person>, callback: GridDataProviderCallback<Person>) => {
30+
const { page, pageSize, parentItem } = params;
31+
const startIndex = page * pageSize;
32+
const endIndex = startIndex + pageSize;
33+
34+
/*
35+
We cannot change the underlying data in this demo so this dataProvider uses
36+
a local field to fetch its values. This allows us to keep a reference to the
37+
modified list instead of loading a new list every time the dataProvider gets
38+
called. In a real application, you should always access your data source
39+
here and avoid using grid.clearCache() whenever possible.
40+
*/
41+
const result = parentItem
42+
? items.value.filter((item) => item.managerId === parentItem.id)
43+
: items.value.filter((item) => item.manager).slice(startIndex, endIndex);
44+
45+
callback(result, result.length);
46+
},
47+
[items.value]
48+
);
49+
50+
return (
51+
<Grid
52+
dataProvider={dataProvider}
53+
itemIdPath="id"
54+
itemHasChildrenPath="manager"
55+
expandedItems={expandedItems.value}
56+
onExpandedItemsChanged={(event) => {
57+
expandedItems.value = event.detail.value;
58+
}}
59+
rowsDraggable
60+
dropMode={draggedItem.value ? 'on-top' : undefined}
61+
onGridDragstart={(event) => {
62+
draggedItem.value = event.detail.draggedItems[0];
63+
}}
64+
onGridDragend={() => {
65+
draggedItem.value = undefined;
66+
}}
67+
onGridDrop={(event) => {
68+
const manager = event.detail.dropTargetItem;
69+
if (draggedItem.value) {
70+
draggedItem.value.managerId = manager.id;
71+
items.value = [...items.value];
72+
}
73+
}}
74+
dragFilter={(model) => {
75+
const item = model.item;
76+
return !item.manager;
77+
}}
78+
dropFilter={(model) => {
79+
const item = model.item;
80+
return item.manager && item.id !== draggedItem.value?.managerId;
81+
}}
82+
>
83+
<GridTreeColumn path="firstName" />
84+
<GridColumn path="lastName" />
85+
<GridColumn path="email" />
86+
</Grid>
87+
);
88+
// end::snippet[]
89+
}
90+
91+
export default reactExample(Example); // hidden-source-line
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import 'Frontend/demo/init'; // hidden-source-line
2+
import '@vaadin/grid';
3+
import '@vaadin/grid/vaadin-grid-tree-column.js';
4+
import { html, LitElement } from 'lit';
5+
import { customElement, query, state } from 'lit/decorators.js';
6+
import type {
7+
Grid,
8+
GridDataProviderCallback,
9+
GridDataProviderParams,
10+
GridDragStartEvent,
11+
GridDropEvent,
12+
GridExpandedItemsChangedEvent,
13+
GridItemModel,
14+
} from '@vaadin/grid';
15+
import { getPeople } from 'Frontend/demo/domain/DataService';
16+
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
17+
import { applyTheme } from 'Frontend/generated/theme';
18+
19+
// tag::snippet[]
20+
@customElement('tree-grid-drag-drop')
21+
export class Example extends LitElement {
22+
protected override createRenderRoot() {
23+
const root = super.createRenderRoot();
24+
// Apply custom theme (only supported if your app uses one)
25+
applyTheme(root);
26+
return root;
27+
}
28+
29+
@query('vaadin-grid')
30+
private grid!: Grid<Person>;
31+
32+
@state()
33+
private draggedItem: Person | undefined;
34+
35+
@state()
36+
private items: Person[] = [];
37+
38+
@state()
39+
private managers: Person[] = [];
40+
41+
@state()
42+
private expandedItems: Person[] = [];
43+
44+
protected override async firstUpdated() {
45+
const { people } = await getPeople();
46+
this.items = people;
47+
this.managers = this.items.filter((item) => item.manager);
48+
// Avoid using this method
49+
this.grid.clearCache();
50+
}
51+
52+
private dataProvider = (
53+
params: GridDataProviderParams<Person>,
54+
callback: GridDataProviderCallback<Person>
55+
) => {
56+
const { page, pageSize, parentItem } = params;
57+
const startIndex = page * pageSize;
58+
const endIndex = startIndex + pageSize;
59+
60+
/*
61+
We cannot change the underlying data in this demo so this dataProvider uses
62+
a local field to fetch its values. This allows us to keep a reference to the
63+
modified list instead of loading a new list every time the dataProvider gets
64+
called. In a real application, you should always access your data source
65+
here and avoid using grid.clearCache() whenever possible.
66+
*/
67+
const result = parentItem
68+
? this.items.filter((item) => item.managerId === parentItem.id)
69+
: this.managers.slice(startIndex, endIndex);
70+
71+
callback(result, result.length);
72+
};
73+
74+
protected override render() {
75+
return html`
76+
<vaadin-grid
77+
.dataProvider="${this.dataProvider}"
78+
.itemIdPath="${'id'}"
79+
.itemHasChildrenPath="${'manager'}"
80+
.expandedItems="${this.expandedItems}"
81+
@expanded-items-changed="${(event: GridExpandedItemsChangedEvent<Person>) => {
82+
this.expandedItems = event.detail.value;
83+
}}"
84+
rows-draggable
85+
.dropMode=${this.draggedItem ? 'on-top' : undefined}
86+
@grid-dragstart="${(event: GridDragStartEvent<Person>) => {
87+
this.draggedItem = event.detail.draggedItems[0];
88+
}}"
89+
@grid-dragend="${() => {
90+
this.draggedItem = undefined;
91+
}}"
92+
@grid-drop="${(event: GridDropEvent<Person>) => {
93+
const manager = event.detail.dropTargetItem;
94+
if (this.draggedItem) {
95+
// In a real application, when using a data provider, you should
96+
// change the persisted data instead of updating a field
97+
this.draggedItem.managerId = manager.id;
98+
// Avoid using this method
99+
this.grid.clearCache();
100+
}
101+
}}"
102+
.dragFilter="${(model: GridItemModel<Person>) => {
103+
const item = model.item;
104+
return !item.manager; // Only drag non-managers
105+
}}"
106+
.dropFilter="${(model: GridItemModel<Person>) => {
107+
const item = model.item;
108+
return (
109+
item.manager && // Can only drop on a supervisor
110+
item.id !== this.draggedItem?.managerId // Disallow dropping on the same manager
111+
);
112+
}}"
113+
>
114+
<vaadin-grid-tree-column path="firstName"></vaadin-grid-tree-column>
115+
<vaadin-grid-column path="lastName"></vaadin-grid-column>
116+
<vaadin-grid-column path="email"></vaadin-grid-column>
117+
</vaadin-grid>
118+
`;
119+
}
120+
}
121+
// end::snippet[]

frontend/demo/init-flow-components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import '@vaadin/flow-frontend/contextMenuTargetConnector.js';
1717
import '@vaadin/flow-frontend/datepickerConnector.js';
1818
import '@vaadin/flow-frontend/disableOnClickFunctions.js';
1919
import '@vaadin/flow-frontend/gridConnector.js';
20+
import '@vaadin/flow-frontend/treeGridConnector.js';
2021
import '@vaadin/flow-frontend/vaadin-grid-flow-selection-column.js';
2122
import '@vaadin/flow-frontend/gridProConnector.js';
2223
import '@vaadin/flow-frontend/vaadin-map/mapConnector.js';
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.vaadin.demo.component.treegrid;
2+
3+
import com.vaadin.demo.DemoExporter; // hidden-source-line
4+
import com.vaadin.demo.domain.DataService;
5+
import com.vaadin.demo.domain.Person;
6+
import com.vaadin.flow.component.grid.dnd.GridDropMode;
7+
import com.vaadin.flow.component.html.Div;
8+
import com.vaadin.flow.component.treegrid.TreeGrid;
9+
import com.vaadin.flow.data.provider.hierarchy.TreeData;
10+
import com.vaadin.flow.data.provider.hierarchy.TreeDataProvider;
11+
import com.vaadin.flow.router.Route;
12+
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.stream.Collectors;
17+
18+
@Route("tree-grid-drag-drop")
19+
public class TreeGridDragDrop extends Div {
20+
21+
private final List<Person> managers;
22+
private final Map<Integer, List<Person>> staffGroupedByMangers;
23+
24+
private Person draggedItem;
25+
26+
public TreeGridDragDrop() {
27+
List<Person> people = DataService.getPeople();
28+
managers = people.stream().filter(Person::isManager)
29+
.collect(Collectors.toList());
30+
staffGroupedByMangers = people.stream()
31+
.filter(person -> person.getManagerId() != null)
32+
.collect(Collectors.groupingBy(Person::getManagerId,
33+
Collectors.toList()));
34+
35+
// tag::snippet[]
36+
TreeGrid<Person> treeGrid = setupTreeGrid();
37+
38+
TreeData<Person> treeData = new TreeData<>();
39+
treeData.addItems(managers, this::getStaff);
40+
41+
// For drag-and-drop use cases, it is recommended to use data providers that
42+
// return hierarchical data in HierarchyFormat.FLATTENED. This format allows
43+
// TreeGrid to maintain the scroll position after refreshAll(), avoiding the
44+
// scroll jumps that can otherwise occur with HierarchyFormat.NESTED (default)
45+
// which requires each hierarchy level to be requested separately, on demand.
46+
TreeDataProvider<Person> treeDataProvider = new TreeDataProvider<>(
47+
treeData/* , HierarchyFormat.FLATTENED */);
48+
treeGrid.setDataProvider(treeDataProvider);
49+
50+
// Enable drag-and-drop
51+
treeGrid.setRowsDraggable(true);
52+
// Only allow dragging staff
53+
treeGrid.setDragFilter(person -> !person.isManager());
54+
// Only allow dropping on managers
55+
treeGrid.setDropFilter(person -> person.isManager());
56+
57+
treeGrid.addDragStartListener(e -> {
58+
treeGrid.setDropMode(GridDropMode.ON_TOP);
59+
draggedItem = e.getDraggedItems().get(0);
60+
});
61+
62+
treeGrid.addDropListener(e -> {
63+
Person newManager = e.getDropTargetItem().orElse(null);
64+
boolean isSameManager = newManager != null
65+
&& newManager.getId().equals(draggedItem.getManagerId());
66+
67+
if (newManager == null || isSameManager)
68+
return;
69+
70+
draggedItem.setManagerId(newManager.getId());
71+
treeData.setParent(draggedItem, newManager);
72+
73+
// Reset TreeGrid's cache to trigger a re-render
74+
treeDataProvider.refreshAll();
75+
});
76+
77+
treeGrid.addDragEndListener(e -> {
78+
treeGrid.setDropMode(null);
79+
draggedItem = null;
80+
});
81+
// end::snippet[]
82+
83+
add(treeGrid);
84+
}
85+
86+
private static TreeGrid<Person> setupTreeGrid() {
87+
TreeGrid<Person> treeGrid = new TreeGrid<>();
88+
treeGrid.addHierarchyColumn(Person::getFirstName)
89+
.setHeader("First name");
90+
treeGrid.addColumn(Person::getLastName).setHeader("Last name");
91+
treeGrid.addColumn(Person::getEmail).setHeader("Email");
92+
93+
return treeGrid;
94+
}
95+
96+
private List<Person> getStaff(Person manager) {
97+
return staffGroupedByMangers.getOrDefault(manager.getId(),
98+
Collections.emptyList());
99+
}
100+
101+
public static class Exporter // hidden-source-line
102+
extends DemoExporter<TreeGridDragDrop> { // hidden-source-line
103+
} // hidden-source-line
104+
}

0 commit comments

Comments
 (0)