Drops can take place on top of rows. This is useful when creating relationships between items or moving an item into another item, such as placing a file inside a folder.
On Top or Between
Drops can occur on top of rows or between them.
Row Reordering
You can drag rows to reorder them. This can be a useful and impressive feature for users. Try dragging with your mouse one of the rows of data in the example here to another place in the list.
import React, { useEffect } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { Avatar } from '@vaadin/react-components/Avatar.js';
import {
Grid,
type GridDragStartEvent,
type GridDropEvent,
} from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
function avatarRenderer({ item }: { item: Person }) {
return <Avatar img={item.pictureUrl} name={`${item.firstName} ${item.lastName}`} />;
}
function Example() {
const items = useSignal<Person[]>([]);
const draggedItem = useSignal<Person | undefined>(undefined);
useEffect(() => {
getPeople().then(({ people }) => {
items.value = people;
});
}, []);
// tag::snippet[]
function handleDragStart(event: GridDragStartEvent<Person>): void {
draggedItem.value = event.detail.draggedItems[0];
}
function handleDragEnd(): void {
draggedItem.value = undefined;
}
function handleDrop(event: GridDropEvent<Person>): void {
const { dropTargetItem, dropLocation } = event.detail;
// Only act when dropping on another item
if (draggedItem.value && dropTargetItem !== draggedItem.value) {
// Remove the item from its previous position
const draggedItemIndex = items.value.indexOf(draggedItem.value);
items.value.splice(draggedItemIndex, 1);
// Re-insert the item at its new position
const dropIndex = items.value.indexOf(dropTargetItem) + (dropLocation === 'below' ? 1 : 0);
items.value.splice(dropIndex, 0, draggedItem.value);
// Re-assign the array to refresh the grid
items.value = [...items.value];
}
}
return (
<Grid
items={items.value}
rowsDraggable
dropMode={draggedItem.value ? 'between' : undefined}
onGridDragstart={handleDragStart}
onGridDragend={handleDragEnd}
onGridDrop={handleDrop}
>
<GridColumn header="Image" flexGrow={0} autoWidth renderer={avatarRenderer} />
<GridColumn path="firstName" />
<GridColumn path="lastName" />
<GridColumn path="email" />
</Grid>
);
// end::snippet[]
}
grid-row-reordering.ts
import '@vaadin/avatar';
import '@vaadin/grid';
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import type { GridDragStartEvent, GridDropEvent } from '@vaadin/grid';
import type { GridColumnBodyLitRenderer } from '@vaadin/grid/lit.js';
import { columnBodyRenderer } from '@vaadin/grid/lit.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';
// tag::snippet[]
@customElement('grid-row-reordering')
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 items: Person[] = [];
@state()
private draggedItem: Person | undefined;
protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people;
}
protected override render() {
return html`
<!-- tag::snippet[] -->
<vaadin-grid
.items="${this.items}"
rows-draggable
.dropMode=${this.draggedItem ? 'between' : undefined}
@grid-dragstart="${(event: GridDragStartEvent<Person>) => {
this.draggedItem = event.detail.draggedItems[0];
}}"
@grid-dragend="${() => {
this.draggedItem = undefined;
}}"
@grid-drop="${(event: GridDropEvent<Person>) => {
const { dropTargetItem, dropLocation } = event.detail;
// Only act when dropping on another item
if (this.draggedItem && dropTargetItem !== this.draggedItem) {
// Remove the item from its previous position
const draggedItemIndex = this.items.indexOf(this.draggedItem);
this.items.splice(draggedItemIndex, 1);
// Re-insert the item at its new position
const dropIndex =
this.items.indexOf(dropTargetItem) + (dropLocation === 'below' ? 1 : 0);
this.items.splice(dropIndex, 0, this.draggedItem);
// Re-assign the array to refresh the grid
this.items = [...this.items];
}
}}"
>
<vaadin-grid-column
header="Image"
flex-grow="0"
auto-width
${columnBodyRenderer(this.avatarRenderer, [])}
></vaadin-grid-column>
<vaadin-grid-column path="firstName"></vaadin-grid-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
</vaadin-grid>
<!-- end::snippet[] -->
`;
}
private avatarRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
<vaadin-avatar
img="${person.pictureUrl}"
name="${person.firstName} ${person.lastName}"
></vaadin-avatar>
`;
}
// end::snippet[]
Drag Rows between Grids
Rows can be dragged from one grid to another. You might use this feature to move, copy or link items from different datasets.
In the example here, there are two grids of data. Maybe they represent people to speak at two different presentations at the same conference. One grid lists the first panel of speakers and the other the second panel. Try dragging people from one to the other, as if you were reassigning them to speak at a different panel.
import React, { useEffect, useMemo } from 'react';
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() {
// 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="on-top"
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[]
}
grid-drag-drop-filters.ts
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 type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';
// tag::snippet[]
@customElement('grid-drag-drop-filters')
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
drop-mode="on-top"
@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[]